2026年5月20日

2026年5月20日

WordPressプラグインで独自テーブルを作る方法|dbDeltaの書き方・更新・$wpdbでの読み書き・削除

WordPress のプラグインで独自のデータ(操作ログ、予約、集計結果など)を大量に扱う場合、投稿メタ(wp_postmeta)やオプション(wp_options)ではなく、専用のテーブルを作ると検索や集計がしやすくなります。この記事では、テーブルの作成に使う dbDelta() の書き方、プラグインを更新したときの列の追加、$wpdb での読み書き、アンインストール時の後片付けを、1つのプラグインの例で説明します。関数の仕様は、2026年11月時点で developer.wordpress.org のプラグインハンドブックと WordPress のソースコード(wp-admin/includes/upgrade.php)で確認したものです。

独自テーブルを作るかどうかの目安

  • 投稿に付く少量の情報(1記事に数個の値)なら、投稿メタで足ります。管理画面や REST API、エクスポートの仕組みもそのまま使えます。
  • 件数が数万件を超えるログや、日付・ユーザーで絞り込んで集計するデータは、独自テーブルにして必要な列にインデックスを付けると速くなります。
  • 独自テーブルは WordPress 標準のエクスポートや多くの移行ツールの対象外になることがあるので、バックアップや移行の手順に含まれるかを確認しておきます。

dbDelta が受け付ける SQL の書き方

dbDelta() は、CREATE TABLE 文を読み、テーブルが無ければ作り、あれば足りない列やインデックスを追加する関数です。SQL を解析する仕組みが単純なので、プラグインハンドブックで次の書き方が求められています。

決まり 例
1行に1つの列を書く user_id bigint(20) unsigned NOT NULL,
PRIMARY KEY と定義の間は半角スペース2つ PRIMARY KEY (id)
インデックスは INDEX ではなく KEY と書き、少なくとも1つ作る KEY user_id (user_id)
列名をアポストロフィーやバッククォートで囲まない action(記号で囲まない)
列の型は小文字で書く bigint(20)、varchar(100)、datetime
CREATE TABLE などの SQL のキーワードは大文字で書く CREATE TABLE、NOT NULL
長さを指定できる型は長さを書く varchar(100)

この決まりから外れると、テーブルは作られても、次に dbDelta() を実行したときに同じ列やインデックスを「違う」と判断して変更しようとしたり、何もしなかったりします。

テーブルを作り、プラグインの更新で列を足す

プラグインのメインファイルに書く例です。テーブルの定義は1か所にまとめ、有効化したときと、プラグインを更新してバージョン番号が変わったときの両方で同じ関数を呼びます。

<?php
/**
 * Plugin Name: My Logs Example
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// テーブルの定義を変えたら、この番号を上げる
const MYPLUGIN_DB_VERSION = '1.1';

function myplugin_install_table() {
	global $wpdb;

	$table_name      = $wpdb->prefix . 'my_logs';
	$charset_collate = $wpdb->get_charset_collate();

	// 1.1 で ip_address 列を追加した
	$sql = "CREATE TABLE {$table_name} (
		id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
		user_id bigint(20) unsigned NOT NULL DEFAULT 0,
		action varchar(100) NOT NULL DEFAULT '',
		description text NOT NULL,
		ip_address varchar(45) NOT NULL DEFAULT '',
		created_at datetime NOT NULL,
		PRIMARY KEY  (id),
		KEY user_id (user_id),
		KEY created_at (created_at)
	) {$charset_collate};";

	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	dbDelta( $sql );

	update_option( 'myplugin_db_version', MYPLUGIN_DB_VERSION );
}

// 有効化したとき
register_activation_hook( __FILE__, 'myplugin_install_table' );

// プラグインを更新したとき(有効化フックは更新では実行されないため)
add_action( 'plugins_loaded', function () {
	if ( get_option( 'myplugin_db_version' ) !== MYPLUGIN_DB_VERSION ) {
		myplugin_install_table();
	}
} );

ポイントは次のとおりです。

  • register_activation_hook() は、管理画面でプラグインを有効化したときだけ動きます。プラグインを新しい版に更新したときは動かないので、プラグインハンドブックの例と同じく、保存しておいたバージョン番号を plugins_loaded で比べ、違うときだけテーブルを作り直します。
  • $wpdb->prefix を使い、テーブル名に wp_ を直接書かないようにします。接頭辞はサイトごとに変えられるためです。
  • $wpdb->get_charset_collate() で、WordPress のほかのテーブルと同じ文字コードと照合順序になります。
  • created_at には初期値を付けず、追加するときに値を入れます(後述)。DEFAULT CURRENT_TIMESTAMP は MySQL や MariaDB のバージョンや設定によって使える型が違うため、WordPress 側で日時を入れるほうが環境の差を受けません。

dbDelta がしてくれること・してくれないこと

wp-admin/includes/upgrade.php の実装では、既存のテーブルに対して次のように動きます。

変更の内容 dbDelta の動き
列を追加する 追加する(ALTER TABLE … ADD COLUMN)
列の型を変える 変更する(CHANGE COLUMN)。ただし text 系・blob 系を小さい型に縮める変更は行わない
列の初期値を変える 変更する(ALTER COLUMN … SET DEFAULT)
インデックスを追加する 追加する
列やインデックスを削除する 行わない。定義から消しても、テーブルには残る
列名を変える 行わない。新しい名前の列が追加され、古い列も残る

列の削除や名前の変更が必要なときは、バージョンを比べる処理の中で、ALTER TABLE を自分で実行します。その前に、本番のデータベースのバックアップを取ってください。

$wpdb で追加・取得・更新・削除する

$wpdb->insert()・update()・delete() は値を自動でエスケープします。自分で SQL を書く get_results() などでは、値を $wpdb->prepare() のプレースホルダー(%d 整数、%s 文字列、%f 小数)で渡します。

<?php
global $wpdb;
$table = $wpdb->prefix . 'my_logs';

// 追加
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';

$result = $wpdb->insert(
	$table,
	array(
		'user_id'     => get_current_user_id(),
		'action'      => 'login',
		'description' => 'ユーザーがログインしました',
		'ip_address'  => $ip ? $ip : '',
		'created_at'  => current_time( 'mysql', true ), // UTC で保存する
	),
	array( '%d', '%s', '%s', '%s', '%s' )
);

if ( false === $result ) {
	error_log( 'my_logs insert failed: ' . $wpdb->last_error );
} else {
	$new_id = $wpdb->insert_id; // 追加した行の id
}

// 取得(値は必ずプレースホルダーで渡す)
$logs = $wpdb->get_results(
	$wpdb->prepare(
		"SELECT id, action, created_at FROM {$table} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d",
		get_current_user_id(),
		10
	)
);

foreach ( $logs as $log ) {
	echo esc_html( $log->action . ' ' . $log->created_at ) . '<br>';
}

// 1件だけ・1つの値だけを取る
$count = (int) $wpdb->get_var(
	$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE action = %s", 'login' )
);

// 更新(第2引数が更新する値、第3引数が WHERE の条件)
$wpdb->update(
	$table,
	array( 'description' => '更新済み' ),
	array( 'id' => 42 ),
	array( '%s' ),
	array( '%d' )
);

// 削除
$wpdb->delete( $table, array( 'id' => 42 ), array( '%d' ) );
  • テーブル名は $wpdb->prefix と固定の文字列から作っているので、SQL に直接埋め込んでいます。利用者の入力をテーブル名や列名に使う場合は、WordPress 6.2 から使える識別子用のプレースホルダー %i を使います。
  • insert()・update()・delete() は、失敗すると false を返します。原因は $wpdb->last_error で確認できます。
  • 日時は current_time( 'mysql', true ) で UTC の Y-m-d H:i:s 形式にして保存し、表示するときにサイトのタイムゾーンへ変換すると、タイムゾーンの設定を変えてもデータがずれません。

$wpdb のほかのメソッドは「WordPressの$wpdbでSQLを実行する方法」で説明しています。

アンインストールしたときにテーブルを消す

プラグインを管理画面で削除したときに、テーブルとオプションを消す処理は、プラグインのフォルダーの直下に置く uninstall.php に書きます。無効化しただけではデータを消さないのが一般的です(再び有効にしたときにデータが残っているように)。

<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
	exit;
}

global $wpdb;

function myplugin_drop_table( $prefix ) {
	global $wpdb;
	$wpdb->query( "DROP TABLE IF EXISTS {$prefix}my_logs" );
}

if ( is_multisite() ) {
	// マルチサイトでは、サイトごとにテーブルとオプションがある
	foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $blog_id ) {
		myplugin_drop_table( $wpdb->get_blog_prefix( $blog_id ) );
		delete_blog_option( $blog_id, 'myplugin_db_version' );
	}
} else {
	myplugin_drop_table( $wpdb->prefix );
	delete_option( 'myplugin_db_version' );
}

マルチサイトでは、$wpdb->prefix がサイトごとに違います(例: wp_2_)。ネットワーク全体で1つのテーブルを使う場合は、代わりに $wpdb->base_prefix を使います。なお、ネットワーク有効化したときも有効化フックは今いるサイトの分しか動かないので、各サイトのテーブルは、上の plugins_loaded のバージョン比較で各サイトが最初に読み込まれたときに作られます。

WP-CLI でテーブルを確かめる

WP-CLI が使えるサーバーでは、次のコマンドで作られたテーブルを確認できます。テーブルの接頭辞は wp db prefix で確認でき、例では wp_ としています。

# 接頭辞を確認する
wp db prefix

# wp_my_ で始まるテーブルの一覧($wpdb に登録されていない独自テーブルも含める)
wp db tables 'wp_my_*' --all-tables-with-prefix

# 列の定義とインデックスを確認する
wp db query "SHOW CREATE TABLE wp_my_logs\G"

# 件数を確認する
wp db query "SELECT COUNT(*) FROM wp_my_logs"

# テーブルごとのサイズを確認する
wp db size --tables --all-tables-with-prefix

列を追加した新しい版を入れた後は、SHOW CREATE TABLE で列が増えたかと、wp option get myplugin_db_version で番号が新しくなったかを確かめます。

注意点

  • 本番で dbDelta() が列の型を変えると、行数の多いテーブルでは時間がかかり、その間テーブルへの書き込みが待たされることがあります。大きなテーブルの変更は、アクセスの少ない時間に行い、事前にバックアップを取ります。
  • テーブルの定義を段階的に変えていく方法(バージョンごとの移行処理)は「WordPressのカスタムテーブルをSCHEMA_VERSIONで管理する方法」で詳しく説明しています。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲