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 に書き、マルチサイトでは全サイトを処理する

関連記事:

お気軽にご相談ください

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