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.