The $wpdb Object

The global database object: table properties, the four read helpers, and the insert/update/delete shorthands.

Syntax$wpdb->get_results( string $query, string $output = OBJECT ): array|null

WordPress 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

MethodReturns
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

Example · php
/**
 * 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.

Example · php
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.

Example · php
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.

Example · php
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

  • Be the first to comment on this lesson.