Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The database and $wpdb

Usually you work with the database in WordPress through ready-made functions: get_posts(), get_post_meta(), get_option(). But reports, complex selections and custom tables need direct access — the $wpdb object.

The WordPress tables

TableWhat it stores
wp_postsPosts, pages, attachments and custom post types
wp_postmetaCustom fields of posts
wp_users, wp_usermetaUsers and their data
wp_optionsSite options
wp_terms, wp_term_taxonomy, wp_term_relationshipsCategories, tags and their links to posts
wp_comments, wp_commentmetaComments

The wp_ prefix may differ — it is set during installation.

Reading

global $wpdb;
 
$count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish'");
 
$rows = $wpdb->get_results("SELECT ID, post_title FROM {$wpdb->posts} ORDER BY post_date DESC LIMIT 5");
 
foreach ($rows as $row) {
    echo esc_html($row->post_title);
}
MethodWhat it returns
get_var()A single value
get_row()A single row
get_col()One column as an array
get_results()All rows

Table names come from the $wpdb->posts, $wpdb->users properties — they already carry the right prefix.

Prepared queries

Values that came from outside are inserted only through prepare():

global $wpdb;
 
$authorId = absint($_GET['author'] ?? 0);
 
$titles = $wpdb->get_col(
    $wpdb->prepare(
        "SELECT post_title FROM {$wpdb->posts} WHERE post_author = %d AND post_status = %s",
        $authorId,
        'publish',
    )
);
MarkerType
%dAn integer
%fA float
%sA string
%iA table or column name

Inserting, updating, deleting

global $wpdb;
 
$table = $wpdb->prefix . 'book_reviews';
 
$wpdb->insert($table, ['book_id' => 15, 'rating' => 5, 'author' => 'Anna'], ['%d', '%d', '%s']);
$reviewId = $wpdb->insert_id;
 
$wpdb->update($table, ['rating' => 4], ['id' => $reviewId], ['%d'], ['%d']);
 
$wpdb->delete($table, ['id' => $reviewId], ['%d']);

These methods escape the values themselves. The third argument lists the column formats.

A custom table

A table is created on plugin activation with the dbDelta() function:

// books.php
 
register_activation_hook(__FILE__, function () {
    global $wpdb;
 
    $table = $wpdb->prefix . 'book_reviews';
    $charset = $wpdb->get_charset_collate();
 
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
 
    dbDelta("CREATE TABLE $table (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        book_id bigint(20) unsigned NOT NULL,
        rating tinyint(1) NOT NULL,
        author varchar(100) NOT NULL,
        created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY book_id (book_id)
    ) $charset;");
});

dbDelta() compares the description with the existing table and applies only the difference. It is picky about the notation: each field on its own line and two spaces after PRIMARY KEY.

Do you need a custom table

Most often — no. A custom post type with custom fields gives you the editor, search, permissions and the REST API without a single SQL query. A custom table is created when there are very many rows or the data is nothing like posts: an event log, ratings, statistics.

Common mistake

A query with a value inserted directly — "... WHERE post_author = $_GET[author]" — opens the way to SQL injection. Pass everything that came from a visitor through $wpdb->prepare(). And do not write a table name with the wp_ prefix by hand: on a site with another prefix the query stops working.

Thanks for reading!

Donate to the site — we work hard for you.