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
| Table | What it stores |
|---|---|
wp_posts | Posts, pages, attachments and custom post types |
wp_postmeta | Custom fields of posts |
wp_users, wp_usermeta | Users and their data |
wp_options | Site options |
wp_terms, wp_term_taxonomy, wp_term_relationships | Categories, tags and their links to posts |
wp_comments, wp_commentmeta | Comments |
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);
}| Method | What 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',
)
);| Marker | Type |
|---|---|
%d | An integer |
%f | A float |
%s | A string |
%i | A 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.