How to Use $wpdb to Run Custom Database Queries in WordPress

WordPress’s WP_Query covers most post retrieval needs, but sometimes you need to query custom tables, run aggregations, or update rows directly. The global $wpdb object wraps all MySQL interactions and handles prepared statements, escaping, and error handling.

Problem: How do you run custom SQL queries in WordPress safely, without exposing the database to SQL injection?

Solution: Use the global $wpdb object — call $wpdb->get_results() for SELECT queries and always pass dynamic values through $wpdb->prepare() to parameterise them before execution.

global $wpdb;

// SELECT multiple rows
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_status = %s LIMIT %d",
        'publish',
        10
    )
);

// SELECT a single row
$row = $wpdb->get_row(
    $wpdb->prepare( "SELECT * FROM {$wpdb->users} WHERE ID = %d", $user_id )
);

// SELECT a single value
$count = $wpdb->get_var(
    "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish'"
);

// INSERT
$wpdb->insert(
    $wpdb->prefix . 'my_custom_table',
    [ 'user_id' => 42, 'score' => 100 ],
    [ '%d',          '%d' ]  // format for each column
);
$new_id = $wpdb->insert_id;

// UPDATE
$wpdb->update(
    $wpdb->prefix . 'my_custom_table',
    [ 'score' => 200 ],  // data
    [ 'user_id' => 42 ], // WHERE
    [ '%d' ],            // data format
    [ '%d' ]             // WHERE format
);

// DELETE
$wpdb->delete(
    $wpdb->prefix . 'my_custom_table',
    [ 'user_id' => 42 ],
    [ '%d' ]
);

NOTE: Always use $wpdb->prepare() for any query that includes user-supplied values. Never interpolate variables directly into SQL strings. Use $wpdb->prefix instead of hardcoding the table prefix — it respects whatever prefix is set in wp-config.php.