2026年10月2日
2026年10月2日
WordPressのWP_Queryメタクエリを最適化してデータベース負荷を軽減する方法
はじめに
WordPressのカスタムフィールド(メタデータ)を使った検索・フィルタリングは非常に便利ですが、データが増えるにつれてデータベースへの負荷が増大し、ページの表示速度が低下する原因になります。この記事では、WP_Query のメタクエリを最適化してパフォーマンスを改善する方法を5ステップで解説します。
症状・原因
meta_queryを含むWP_Queryの実行が遅く、ページ表示に数秒かかるwp_postmetaテーブルにインデックスが不足しており、フルテーブルスキャンが発生している- 同じメタクエリを毎回実行しており、キャッシュが活用されていない
- 複雑なJOINが必要なクエリを
meta_queryで実装しようとしている
解決手順
ステップ1:meta_query の基本構文を理解する
まず、meta_query の基本的な書き方を確認します。key・value・compare・type の組み合わせが重要です。
// 基本的なmeta_queryの書き方
$query = new WP_Query([
'post_type' => 'product',
'meta_query' => [
'relation' => 'AND', // OR も指定可能
// 単純な値の比較
[
'key' => 'price',
'value' => 1000,
'compare' => '>=', // =, !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN
'type' => 'NUMERIC', // NUMERIC, BINARY, CHAR, DATE, DATETIME, DECIMAL, SIGNED, UNSIGNED, TIME
],
// 複数値のIN検索
[
'key' => 'status',
'value' => ['active', 'featured'],
'compare' => 'IN',
],
// 範囲指定(BETWEEN)
[
'key' => 'stock',
'value' => [10, 100],
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
],
],
'orderby' => 'meta_value_num',
'meta_key' => 'price',
'order' => 'ASC',
]);
// 名前付きクエリ(orderbyに利用)
$query = new WP_Query([
'post_type' => 'product',
'meta_query' => [
'price_clause' => [ // クエリに名前をつける
'key' => 'price',
'type' => 'NUMERIC',
],
],
'orderby' => ['price_clause' => 'ASC'], // 名前で参照
]);
ステップ2:'compare' => 'EXISTS' を活用する
メタキーの存在確認をする場合、値の比較より EXISTS を使う方が高速です。!= '' のような空文字比較は避けましょう。
// 悪い例:値の比較で存在確認(遅い)
$bad_query = new WP_Query([
'meta_query' => [
[
'key' => 'featured_image_id',
'value' => '',
'compare' => '!=', // フルスキャンが発生しやすい
],
],
]);
// 良い例:EXISTS で存在確認(速い)
$good_query = new WP_Query([
'meta_query' => [
[
'key' => 'featured_image_id',
'compare' => 'EXISTS', // キーが存在する投稿のみ
],
],
]);
// NOT EXISTS で存在しない投稿を取得
$missing_meta_query = new WP_Query([
'meta_query' => [
[
'key' => 'price',
'compare' => 'NOT EXISTS', // priceメタが未設定の投稿
],
],
]);
// EXISTS と値比較を組み合わせる
$combined_query = new WP_Query([
'meta_query' => [
'relation' => 'AND',
[
'key' => 'price',
'compare' => 'EXISTS',
],
[
'key' => 'price',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
],
]);
ステップ3:MySQL インデックスを追加してクエリを高速化する
wp_postmeta テーブルの meta_value カラムにインデックスを追加することで、特定のメタキーへのクエリを大幅に高速化できます。
// プラグイン有効化時にインデックスを作成
function mytheme_create_postmeta_index() {
global $wpdb;
// 既存のインデックスを確認
$index_exists = $wpdb->get_results(
"SHOW INDEX FROM {$wpdb->postmeta}
WHERE Key_name = 'idx_meta_key_price'"
);
if ( empty($index_exists) ) {
// meta_key と meta_value の複合インデックスを作成
$wpdb->query(
"CREATE INDEX idx_meta_key_price
ON {$wpdb->postmeta} (meta_key(50), meta_value(20))"
);
// 数値データ用の別インデックス(ORDER BY meta_value_num用)
// ※ meta_value はVARCHARなので、数値ソートには別テーブル検討も
}
}
register_activation_hook(__FILE__, 'mytheme_create_postmeta_index');
// インデックス削除(プラグイン無効化時)
function mytheme_drop_postmeta_index() {
global $wpdb;
$wpdb->query(
"DROP INDEX IF EXISTS idx_meta_key_price
ON {$wpdb->postmeta}"
);
}
register_deactivation_hook(__FILE__, 'mytheme_drop_postmeta_index');
// EXPLAIN でクエリを分析(開発時のみ)
function mytheme_analyze_query( $query_string ) {
global $wpdb;
$results = $wpdb->get_results("EXPLAIN " . $query_string);
// type が 'ALL' の場合はフルスキャン(要改善)
// type が 'ref' や 'range' の場合はインデックス使用(良い)
return $results;
}
ステップ4:Transient キャッシュで繰り返しのクエリを回避する
同じメタクエリを何度も実行している場合は、Transient API でキャッシュして負荷を軽減します。
function mytheme_get_featured_products( $limit = 12 ) {
// キャッシュキーを生成
$cache_key = 'featured_products_' . $limit;
// キャッシュからデータを取得
$products = get_transient($cache_key);
if ( false === $products ) {
// キャッシュがない場合のみクエリを実行
$query = new WP_Query([
'post_type' => 'product',
'posts_per_page' => $limit,
'post_status' => 'publish',
'meta_query' => [
[
'key' => 'is_featured',
'value' => '1',
'compare' => '=',
],
],
'orderby' => 'date',
'order' => 'DESC',
// メモリ節約のため不要なフィールドは省く
'no_found_rows' => true, // ページネーション不要なら高速化
'update_post_meta_cache' => false, // メタキャッシュを別途管理
]);
$products = $query->posts;
// 6時間キャッシュ(商品更新頻度に合わせて調整)
set_transient($cache_key, $products, 6 * HOUR_IN_SECONDS);
}
return $products;
}
// 商品更新時にキャッシュを削除
function mytheme_clear_products_cache( $post_id ) {
if ( get_post_type($post_id) === 'product' ) {
delete_transient('featured_products_12');
delete_transient('featured_products_6');
}
}
add_action('save_post_product', 'mytheme_clear_products_cache');
ステップ5:posts_join / posts_where フィルターで複雑なJOINを実装する
複雑な条件や複数テーブルの結合が必要な場合、meta_query の代わりに posts_join と posts_where フィルターを使って直接SQLを記述します。
// 複数のメタ値を効率的にJOINする例
function mytheme_complex_meta_join( $join, $query ) {
global $wpdb;
if ( is_admin() || ! $query->is_main_query() ) {
return $join;
}
if ( $query->get('mytheme_price_filter') ) {
// 通常のmeta_queryは複数のLEFT JOINを生成するが
// カスタムJOINで一度にまとめて取得
$join .= " INNER JOIN {$wpdb->postmeta} AS price_meta
ON ({$wpdb->posts}.ID = price_meta.post_id
AND price_meta.meta_key = 'price')";
}
return $join;
}
add_filter('posts_join', 'mytheme_complex_meta_join', 10, 2);
function mytheme_complex_meta_where( $where, $query ) {
global $wpdb;
if ( is_admin() || ! $query->is_main_query() ) {
return $where;
}
if ( $query->get('mytheme_price_filter') ) {
$min = floatval($query->get('price_min'));
$max = floatval($query->get('price_max'));
$where .= $wpdb->prepare(
" AND CAST(price_meta.meta_value AS DECIMAL(10,2))
BETWEEN %f AND %f",
$min,
$max
);
}
return $where;
}
add_filter('posts_where', 'mytheme_complex_meta_where', 10, 2);
// 使用例
$query = new WP_Query([
'post_type' => 'product',
'mytheme_price_filter' => true,
'price_min' => 1000,
'price_max' => 5000,
]);
注意事項
$wpdb->query()で直接SQLを実行する場合は、必ず$wpdb->prepare()でパラメーターをエスケープしてください。SQLインジェクションの原因になります。- インデックスの追加はパフォーマンスを改善しますが、INSERT/UPDATE/DELETEのオーバーヘッドが増えます。書き込みが多いテーブルでは慎重に判断してください。
- Transientキャッシュはオブジェクトキャッシュ(Redis・Memcachedなど)が導入されていない場合、データベースの
wp_optionsテーブルに保存されます。大量のTransientはかえって負荷になることがあります。
まとめ
WP_Queryのメタクエリ最適化は、EXISTS の活用、MySQLインデックスの追加、Transientキャッシュの3点が特に効果的です。複雑なクエリは posts_join・posts_where フィルターで直接SQLを記述することで柔軟に対応できます。関連記事:WordPressマルチサイトネットワークの構築と管理方法