2026年7月22日
2026年7月22日
WordPressで独自のデータベーステーブルを作る方法|dbDeltaと$wpdbの使い方
WordPressのデータは、投稿は wp_posts、追加情報は wp_postmeta のように、決まったテーブルに保存されます。アクセスログや集計値のように件数が多く、投稿とは形の違うデータは、独自のテーブルを作って保存したほうが検索も削除も速くなります。この記事では、閲覧記録を保存するテーブルを例に、プラグインでテーブルを作り、読み書きし、削除するまでの手順を説明します。内容は2026年10月時点のWordPress 6系とPHP 8系を前提にしています。
独自テーブルにするかを先に決める
独自テーブルは、WordPressのエクスポート機能やREST API、多くのバックアップ以外のプラグインからは扱われません。次のように使い分けます。
| データ | 保存先の目安 |
|---|---|
| 管理画面で編集する記事のようなデータ | カスタム投稿タイプとカスタムフィールド |
| 1件だけの設定値 | wp_options(get_option()/update_option()) |
| 件数が多い記録(ログ、集計、予約枠など)で、列で絞り込んだり期間で削除したりするもの | 独自テーブル |
テーブルを作る(dbDelta)
テーブルの作成には、WordPressの dbDelta() 関数を使います。テーブルがなければ作り、すでにあれば、渡した定義との差(列やインデックスの追加・変更)だけを反映します。列を削除することはしません。
次のコードは、プラグインのメインファイル wp-content/plugins/my-access-log/my-access-log.php として保存する想定です。register_activation_hook() はプラグインのメインファイルでしか動かないため、テーマの functions.php には書けません。
<?php
/*
* Plugin Name: My Access Log
* Description: 投稿の閲覧を独自テーブルに記録するサンプル
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// テーブル構造のバージョン。構造を変えたら上げる
define( 'MY_ACCESS_LOG_DB_VERSION', '1.0' );
// テーブル名(接頭辞つき)を返す
function my_access_log_table() {
global $wpdb;
return $wpdb->prefix . 'my_access_log';
}
// テーブルを作成・更新する
function my_access_log_install() {
global $wpdb;
$table = my_access_log_table();
$charset_collate = $wpdb->get_charset_collate();
// dbDelta の書式に合わせる(1行に1列、PRIMARY KEY の後は空白2つ、KEY を使う)
$sql = "CREATE TABLE $table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_id bigint(20) unsigned NOT NULL DEFAULT 0,
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
accessed_at datetime NOT NULL,
PRIMARY KEY (id),
KEY post_id (post_id),
KEY accessed_at (accessed_at)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
update_option( 'my_access_log_db_version', MY_ACCESS_LOG_DB_VERSION, false );
}
// 有効化したときに作る
register_activation_hook( __FILE__, 'my_access_log_install' );
// 更新でバージョンが変わったときにも作り直す(有効化フックは更新時には動かない)
add_action( 'plugins_loaded', function () {
if ( get_option( 'my_access_log_db_version' ) !== MY_ACCESS_LOG_DB_VERSION ) {
my_access_log_install();
}
} );
テーブル名には $wpdb->prefix を付けます。接頭辞を wp_ 以外にしているサイトや、マルチサイトのサブサイト(接頭辞が wp_2_ のようになる)でも、正しいテーブル名になります。$wpdb->get_charset_collate() は、WordPressのほかのテーブルと同じ文字コードと照合順序を指定するためのものです。
dbDeltaの書式の決まり
dbDelta() はSQLを文字列として解析して差を求めるため、書式に決まりがあります。守らないと、毎回「差がある」と判断されたり、インデックスが重複して作られたりします。
- 1行に1つの列を書く
PRIMARY KEYと主キーの定義の間は空白を2つ空ける- インデックスは
INDEXではなくKEYと書き、少なくとも1つ置く - 列名をバッククォートや引用符で囲まない
- 列の型は小文字(
bigint、varchar)、CREATE TABLEなどのSQLのキーワードは大文字で書く - 長さを指定できる型には長さを書く(
bigint(20)、varchar(255)など)
テーブルの構造を変える
列を追加したいときは、CREATE TABLE の定義に列を書き足し、バージョンの定数を上げます。次にプラグインが読み込まれたときに plugins_loaded の確認でバージョンの違いが見つかり、dbDelta() が列を追加します。
<?php
// 変更例:バージョンを 1.1 に上げ、定義に referer 列を追加する
define( 'MY_ACCESS_LOG_DB_VERSION', '1.1' );
// my_access_log_install() の $sql の中に、次の1行を追加する
// referer varchar(255) NOT NULL DEFAULT '',
ALTER TABLE を直接実行する方法もありますが、同じ処理が2回動くとエラーになるため、dbDelta() に任せるほうが安全です。列の削除や名前の変更は dbDelta() ではできないので、その場合だけ ALTER TABLE を使い、実行済みかどうかをバージョン番号で管理します。
データを読み書きする($wpdb)
$wpdb には、配列を渡すだけで安全にSQLを組み立てるメソッドがあります。値をSQLの文字列に直接つなげると、SQLインジェクションの原因になります。値は次のメソッドの引数か、$wpdb->prepare() のプレースホルダーで渡してください。
<?php
// 投稿の閲覧を1件記録する
add_action( 'template_redirect', function () {
if ( ! is_singular( 'post' ) ) {
return;
}
global $wpdb;
$result = $wpdb->insert(
my_access_log_table(),
array(
'post_id' => get_queried_object_id(),
'user_id' => get_current_user_id(),
'accessed_at' => current_time( 'mysql', true ), // UTC で保存する
),
array( '%d', '%d', '%s' )
);
if ( false === $result ) {
error_log( 'my_access_log insert failed: ' . $wpdb->last_error );
}
} );
// 投稿ごとの直近の記録を取得する(%i は WordPress 6.2 以上)
function my_access_log_recent( $post_id, $limit = 20 ) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
'SELECT id, user_id, accessed_at FROM %i WHERE post_id = %d ORDER BY accessed_at DESC LIMIT %d',
my_access_log_table(),
$post_id,
$limit
),
ARRAY_A
);
}
// 投稿ごとの件数(1つの値を取得する)
function my_access_log_count( $post_id ) {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
'SELECT COUNT(*) FROM %i WHERE post_id = %d',
my_access_log_table(),
$post_id
)
);
}
// 記録の投稿IDを付け替える(更新)
function my_access_log_move( $from_post_id, $to_post_id ) {
global $wpdb;
return $wpdb->update(
my_access_log_table(),
array( 'post_id' => $to_post_id ), // 変更する列と値
array( 'post_id' => $from_post_id ), // WHERE の条件
array( '%d' ),
array( '%d' )
);
}
// 指定した日数より古い記録を削除する
function my_access_log_purge( $days = 90 ) {
global $wpdb;
$limit = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS );
return $wpdb->query(
$wpdb->prepare(
'DELETE FROM %i WHERE accessed_at < %s',
my_access_log_table(),
$limit
)
);
}
// 投稿を削除したら、その投稿の記録も消す
add_action( 'deleted_post', function ( $post_id ) {
global $wpdb;
$wpdb->delete( my_access_log_table(), array( 'post_id' => $post_id ), array( '%d' ) );
} );
主なメソッドと戻り値は次のとおりです。
| メソッド | 用途 | 戻り値 |
|---|---|---|
$wpdb->insert( テーブル, データ, 形式 ) |
1行を追加 | 追加した行数。失敗時は false。追加した行のIDは $wpdb->insert_id |
$wpdb->update( テーブル, データ, 条件, 形式, 条件の形式 ) |
条件に合う行を更新 | 更新した行数。値が同じで変わらなかった場合は 0、失敗時は false |
$wpdb->delete( テーブル, 条件, 条件の形式 ) |
条件に合う行を削除 | 削除した行数。失敗時は false |
$wpdb->get_results( SQL, ARRAY_A ) |
複数行を取得 | 行の配列 |
$wpdb->get_var( SQL ) |
1つの値を取得 | 値。該当がなければ null |
$wpdb->query( SQL ) |
任意のSQLを実行 | 影響した行数。失敗時は false |
update() は、値が変わらなかったときにも 0 を返します。成功と失敗を分けるときは、false === $result で判定してください。
プレースホルダーの使い方
$wpdb->prepare() で使えるプレースホルダーは次の4つです。
| プレースホルダー | 意味 |
|---|---|
%d |
整数 |
%f |
小数 |
%s |
文字列(引用符は prepare() が付けるので、自分で '%s' と囲まない) |
%i |
テーブル名や列名などの識別子(WordPress 6.2以上) |
LIKE で部分一致を検索するときは、% を含めた検索語を引数として渡し、利用者が入力した文字は $wpdb->esc_like() で処理します。
<?php
// 1.1 で追加した referer 列に、検索語を含む記録を探す
global $wpdb;
$like = '%' . $wpdb->esc_like( $keyword ) . '%';
$rows = $wpdb->get_results(
$wpdb->prepare( 'SELECT id FROM %i WHERE referer LIKE %s', my_access_log_table(), $like )
);
運用上の注意
- ページキャッシュのプラグインやサーバーのキャッシュを使っていると、キャッシュから表示されたページではPHPが動かず、
template_redirectでの記録は行われません。正確な件数が必要な場合は、JavaScriptからREST APIを呼ぶなど、キャッシュの影響を受けない方法で記録します - 閲覧のたびに書き込むと、アクセスが多いサイトではデータベースの負荷が上がります。古い記録は
my_access_log_purge()のような処理をWP-Cronで定期的に実行して削除します - IPアドレスなど個人を特定できる情報を保存する場合は、プライバシーポリシーへの記載や保存期間を決めてから保存してください
プラグイン削除時にテーブルを消す
プラグインを削除したときにテーブルとオプションを消すには、プラグインのフォルダーに uninstall.php を置きます。このファイルは、管理画面でプラグインを「削除」したときだけ実行され、無効化しただけでは実行されません。
<?php
// uninstall.php:プラグインを削除したときだけ WordPress が実行する
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
// 現在のサイトのテーブルとオプションを消す
function my_access_log_uninstall_site() {
global $wpdb;
$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'my_access_log' ) );
delete_option( 'my_access_log_db_version' );
}
if ( is_multisite() ) {
// マルチサイトでは全サイトを順に処理する
foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $site_id ) {
switch_to_blog( $site_id );
my_access_log_uninstall_site();
restore_current_blog();
}
} else {
my_access_log_uninstall_site();
}
マルチサイトでは、switch_to_blog() でサイトを切り替えると $wpdb->prefix もそのサイトの接頭辞に変わるため、サイトごとのテーブルを順に削除できます。作成側も、このプラグインでは plugins_loaded でサイトごとにバージョンを確認しているため、各サイトで初めて読み込まれたときにテーブルが作られます。サイト数が多い場合は、サイトの数だけ処理が走ることに注意してください。
まとめ
- 件数が多く、投稿と形の違うデータは独自テーブルに保存する。編集する記事のようなデータはカスタム投稿タイプにする
- テーブルは
dbDelta()で作る。1行1列、PRIMARY KEYの後は空白2つ、KEYを使うなど書式の決まりを守る - バージョン番号をオプションに保存し、
plugins_loadedで確認してdbDelta()を再実行する。有効化フックは更新時には動かない - 値は
$wpdb->insert()・update()・delete()の引数か、prepare()のプレースホルダーで渡す。テーブル名は%i(6.2以上)で渡せる update()の0は失敗ではない。判定はfalse ===で行う- 削除時の後片付けは
uninstall.phpに書き、マルチサイトでは全サイトを処理する
関連記事: