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マルチサイトネットワークの構築と管理方法

お気軽にご相談ください

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