2026年7月11日
2026年7月11日
WordPressのデータベースクエリエラーを解決する方法
はじめに
WordPressでWP_Queryで期待した投稿が取得できない・$wpdb->query()でSQLを実行しているが結果がfalseになる・$wpdb->prepare()の使い方が分からずSQLインジェクションのリスクがある・カスタムクエリがタイムアウトするほど遅いといった問題は、WordPressのデータベースAPIを正しく使い、適切なインデックスとクエリ最適化を行うことで解決できます。
症状・原因
$wpdb->prepare()を使わず文字列連結でSQLを組み立てており、SQLインジェクションの脆弱性があるWP_Queryのmeta_queryやtax_queryが複雑になりすぎてN+1問題が発生している$wpdb->last_errorを確認せずクエリの失敗原因が不明なままになっている- カスタムテーブルにインデックスがなく全テーブルスキャンが発生している
解決手順
ステップ1:データベースクエリの状態を診断する
# データベース接続を確認
wp db check
# 遅いクエリを確認(SAVEQUERIES を有効化)
wp eval "
define('SAVEQUERIES', true);
global \$wpdb;
// クエリ実行後...
foreach (\$wpdb->queries as \$q) {
if (\$q[1] > 0.01) { // 10ms以上かかったクエリ
echo round(\$q[1] * 1000) . 'ms: ' . \$q[0] . PHP_EOL;
}
}
"
# 最後のクエリエラーを確認
wp eval "global \$wpdb; echo \$wpdb->last_error;"
# テーブルの状態を確認
wp db query "SHOW TABLE STATUS LIKE 'wp_posts';"
wp db query "EXPLAIN SELECT * FROM wp_posts WHERE post_status='publish' LIMIT 10;"
ステップ2:$wpdbを正しく使用する
global $wpdb;
// ✅ 正しい: $wpdb->prepare() でSQLインジェクション対策
$user_id = 5;
$status = 'publish';
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_author = %d
AND post_status = %s
AND post_type = 'post'
ORDER BY post_date DESC
LIMIT %d",
$user_id,
$status,
10
)
);
// ❌ 悪い例: SQLインジェクション脆弱性あり
// $results = $wpdb->get_results("SELECT * FROM {$wpdb->posts} WHERE ID = " . $_GET['id']);
// エラーチェック
if ($wpdb->last_error) {
error_log('DB Error: ' . $wpdb->last_error);
return [];
}
// INSERT / UPDATE / DELETE
$wpdb->insert(
$wpdb->prefix . 'my_table',
['user_id' => $user_id, 'data' => 'value', 'created' => current_time('mysql')],
['%d', '%s', '%s']
);
$inserted_id = $wpdb->insert_id;
$wpdb->update(
$wpdb->prefix . 'my_table',
['data' => 'new_value'],
['user_id' => $user_id],
['%s'],
['%d']
);
ステップ3:WP_Queryを最適化する
// パフォーマンスを意識したWP_Query
$query = new WP_Query([
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 20,
'no_found_rows' => true, // ページネーション不要時はtrueでCOUNTクエリを省略
'update_post_meta_cache' => true, // メタデータを一括取得
'update_post_term_cache' => true, // タームデータを一括取得
'fields' => 'ids', // IDのみ必要な場合
// meta_query はインデックスが有効な列のみ使用
'meta_query' => [
[
'key' => '_price',
'value' => [100, 10000],
'type' => 'NUMERIC',
'compare' => 'BETWEEN',
],
],
// tax_query
'tax_query' => [
[
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => ['electronics'],
],
],
]);
// N+1問題を避ける: メタデータは一括取得
if ($query->have_posts()) {
$post_ids = wp_list_pluck($query->posts, 'ID');
// update_meta_cache で一括取得
update_postmeta_cache($post_ids);
}
ステップ4:カスタムテーブルを作成・管理する
// カスタムテーブルをdbDelta()で作成・更新
function create_my_custom_table(): void {
global $wpdb;
$table_name = $wpdb->prefix . 'my_logs';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table_name} (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
action varchar(50) NOT NULL,
data longtext,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY action (action),
KEY created_at (created_at)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
register_activation_hook(__FILE__, 'create_my_custom_table');
// テーブルバージョン管理
$current_version = get_option('my_plugin_db_version', '0');
if (version_compare($current_version, '1.1', '<')) {
create_my_custom_table();
update_option('my_plugin_db_version', '1.1');
}
ステップ5:クエリキャッシュで高速化する
// wp_cache_get/set でクエリ結果をキャッシュ
function get_top_products(int $limit = 10): array {
$cache_key = "top_products_{$limit}";
$cache_group = 'my_plugin';
$cached = wp_cache_get($cache_key, $cache_group);
if ($cached !== false) {
return $cached;
}
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title, pm.meta_value as price
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = 'product'
AND p.post_status = 'publish'
AND pm.meta_key = '_price'
ORDER BY CAST(pm.meta_value AS DECIMAL) DESC
LIMIT %d",
$limit
)
);
wp_cache_set($cache_key, $results, $cache_group, HOUR_IN_SECONDS);
return $results ?: [];
}
注意事項
$wpdb->prepare()を使わずに変数をSQL文字列に直接埋め込むことは絶対に避けてください。WordPressは$wpdb->last_errorにエラー詳細を格納しますが、本番環境ではWP_DEBUGを無効にしてエラーをユーザーに表示しないようにしてください- カスタムテーブルのスキーマ変更には必ず
dbDelta()を使用してください。直接ALTER TABLEを実行すると、プラグインのバージョンアップ時に既存データが失われる可能性があります
まとめ
WordPress データベースクエリ問題の解決は①wp db checkでDB接続確認・SAVEQUERIESで遅いクエリを特定・$wpdb->last_errorでエラー確認・EXPLAINでクエリ実行計画を分析、②$wpdb->prepare()で%d/%s/%fプレースホルダーを使用・変数の直接埋め込み禁止・insert/update/deleteメソッドでデータ型を指定、③no_found_rows=trueでCOUNTクエリ省略・fields='ids'でIDのみ取得・update_post_meta_cacheでN+1問題解消・meta_queryのインデックス活用、④dbDelta()でカスタムテーブル作成・PRIMARY KEY/インデックスを適切に設定・バージョン管理で安全なスキーマ更新、⑤wp_cache_get/setでクエリ結果をキャッシュ・JOINクエリでN+1を防止・HOUR_IN_SECONDSで有効期限管理の手順で解決します。