The $wpdb Object
The global database object: table properties, the four read helpers, and the insert/update/delete shorthands.
$wpdb->get_results( string $query, string $output = OBJECT ): array|nullWordPress ships no ORM. Direct SQL goes through $wpdb, a global instance of the wpdb class built during bootstrap from the credentials in wp-config.php. Declare global $wpdb; inside any function that needs it. Reach for it only when WP_Query, the meta API or the options API cannot express the query — those APIs add caching that raw SQL bypasses.
Table names
Every core table has a property holding the already-prefixed name: $wpdb->posts, $wpdb->postmeta, $wpdb->options, $wpdb->users, $wpdb->usermeta, $wpdb->terms, $wpdb->term_taxonomy, $wpdb->term_relationships, $wpdb->termmeta, $wpdb->comments, $wpdb->commentmeta and $wpdb->links. Custom tables use $wpdb->prefix . 'name'.
Reading
| Method | Returns |
|---|---|
get_var( $query, $x = 0, $y = 0 ) | One cell as string, or null |
get_row( $query, $output = OBJECT, $y = 0 ) | One row; OBJECT, ARRAY_A or ARRAY_N |
get_col( $query, $x = 0 ) | One column as a flat array |
get_results( $query, $output = OBJECT ) | All rows; also accepts OBJECT_K |
Every value comes back as a string — MySQL types are not preserved, so cast what you need.
Writing
insert(), update(), delete() and replace() take column/value arrays and build the statement for you. They are shorthand for query() wrapped around prepare(), and each returns the affected row count as an int, or false on error. After an insert, the new key is in $wpdb->insert_id; the last failure message is in $wpdb->last_error.
Example
/**
* The five most recent published posts in a category, with a meta value
* joined in - a query WP_Query would need two round trips to produce.
*
* @return object[] stdClass rows; empty array when nothing matches.
*/
function acme_top_recent( int $term_id ): array {
global $wpdb; // $wpdb is a global, not a static accessor.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title, CAST(pm.meta_value AS UNSIGNED) AS views
FROM {$wpdb->posts} AS p
INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID
INNER JOIN {$wpdb->term_taxonomy} AS tt
ON tt.term_taxonomy_id = tr.term_taxonomy_id
LEFT JOIN {$wpdb->postmeta} AS pm
ON pm.post_id = p.ID AND pm.meta_key = 'acme_views'
WHERE tt.term_id = %d
AND tt.taxonomy = 'category'
AND p.post_status = 'publish'
AND p.post_type = 'post'
ORDER BY p.post_date DESC
LIMIT 5",
$term_id
)
);
}When to use it
- Writing a reporting or export query that WP_Query and get_posts() cannot express.
- Bulk-updating thousands of rows without loading each post object into memory.
- Reading from and writing to a custom table your plugin created.
- Debugging a query by inspecting $wpdb->last_query and $wpdb->last_error.
More examples
insert(), update(), delete() and the SQL they stand for
The helpers cover the common single-table case and remove any chance of forgetting prepare(). Drop to query() + prepare() only when you need multi-row inserts, ON DUPLICATE KEY, or an expression such as views = views + 1 that the helpers cannot express.
global $wpdb;
$table = $wpdb->prefix . 'acme_signups';
// Shorthand: insert() escapes the values and builds the statement.
$result = $wpdb->insert(
$table,
array(
'email' => $email,
'campaign_id' => $campaign_id,
'created' => current_time( 'mysql' ),
),
array( '%s', '%d', '%s' ) // Formats, matched positionally to $data.
);
$new_id = $wpdb->insert_id; // AUTO_INCREMENT value of the row just written.
// Longhand: the same work, done by hand.
$result = $wpdb->query(
$wpdb->prepare(
"INSERT INTO {$table} (email, campaign_id, created) VALUES (%s, %d, %s)",
$email,
$campaign_id,
current_time( 'mysql' )
)
);
// update() takes $data then $where, each with its own format array.
$wpdb->update( $table, array( 'confirmed' => 1 ), array( 'id' => $new_id ), array( '%d' ), array( '%d' ) );
// delete() takes only $where. All conditions are joined with AND.
$wpdb->delete( $table, array( 'id' => $new_id ), array( '%d' ) );
if ( false === $result ) {
error_log( 'Signup write failed: ' . $wpdb->last_error );
}Choosing the right read method and output format
Picking the narrowest method keeps the calling code free of loops and array reshaping. OBJECT_K is the one most people never discover, and it removes an entire foreach when you are building a lookup map.
global $wpdb;
// One cell. Keep the raw value if you need to tell failure from an empty string.
$raw = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
if ( null === $raw && '' !== $wpdb->last_error ) {
error_log( 'User count failed: ' . $wpdb->last_error );
}
$total = (int) $raw; // Everything arrives as a string.
// One row, as an associative array instead of the default stdClass.
$row = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ),
ARRAY_A
);
// One column, flattened - no array_column() needed afterwards.
$child_ids = $wpdb->get_col(
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_parent = %d", $parent_id )
);
// OBJECT_K keys the result set by the first selected column, here ID.
$by_id = $wpdb->get_results(
"SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type = 'page'",
OBJECT_K
);
echo $by_id[42]->post_title;Surfacing errors that are silent by default
When a $wpdb call returns an empty array and you cannot see why, last_query and last_error are the two properties that answer it. Log last_query rather than reconstructing the SQL by hand — it shows the query after placeholder substitution.
global $wpdb;
// wpdb swallows MySQL errors on the front end unless debugging is on.
// Turn them on around a block you are actively debugging:
$wpdb->show_errors();
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}acme_typo" );
$wpdb->hide_errors();
// Or check explicitly, which is what production code should do.
if ( '' !== $wpdb->last_error ) {
error_log( sprintf(
'Query failed: %s | SQL: %s',
$wpdb->last_error,
$wpdb->last_query // The exact SQL sent to MySQL, after prepare().
) );
}
// $wpdb->num_rows holds the row count from the last SELECT.
error_log( 'Rows returned: ' . $wpdb->num_rows );
Discussion