2026年7月1日

2026年7月1日

WordPressのWP_Queryを高度に活用する方法【複雑なクエリ・パフォーマンス最適化】

はじめに

WP_QueryはWordPressの投稿データを取得するための中心的なクラスで、基本的な使い方から非常に複雑な条件のクエリまで対応しています。適切なパラメータと最適化テクニックを組み合わせることで、大規模サイトでも高速に動作するクエリを実装できます。本記事ではtax_querymeta_queryの複合条件からSQLフィルターによるカスタマイズまで解説します。

症状・原因

  • 複数のタクソノミーとカスタムフィールドを組み合わせた複雑な絞り込み条件を実装したい
  • WP_Queryのループ内でページネーションが正常に動作しない
  • 大量の投稿データを処理する際にWP_Queryのパフォーマンスが問題になっている

解決手順

ステップ1:tax_queryでAND/ORと複数タクソノミーを組み合わせる

<?php
/**
 * tax_query:複数のタクソノミー条件を組み合わせる
 *
 * relation: 'AND'(すべての条件を満たす)または 'OR'(いずれかの条件を満たす)
 */

// === 例1:カテゴリ「技術」かつタグ「WordPress」の投稿 ===
$query = new WP_Query( array(
    'post_type'  => 'post',
    'tax_query'  => array(
        'relation' => 'AND',   // すべての条件を満たす投稿を取得
        array(
            'taxonomy' => 'category',      // タクソノミー名
            'field'    => 'slug',          // 'slug' | 'name' | 'term_id' | 'term_taxonomy_id'
            'terms'    => 'technology',    // 単一値または配列
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array( 'wordpress', 'php' ), // 複数タグのOR
            'operator' => 'IN',            // 'IN' | 'NOT IN' | 'AND' | 'EXISTS' | 'NOT EXISTS'
        ),
    ),
) );

// === 例2:ネスト構造を使った複合条件 ===
// (カテゴリ「A」または「B」)かつ(タグ「featured」が付いている)
$query2 = new WP_Query( array(
    'post_type' => 'post',
    'tax_query' => array(
        'relation' => 'AND',
        // グループ1:カテゴリの OR 条件
        array(
            'relation' => 'OR',
            array(
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => 'news',
            ),
            array(
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => 'tutorial',
            ),
        ),
        // グループ2:特定タグが必須
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => 'featured',
        ),
    ),
) );

// === 例3:特定タクソノミーに属さない投稿を取得(NOT IN)===
$query3 = new WP_Query( array(
    'post_type' => 'post',
    'tax_query' => array(
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'uncategorized', 'test' ),
            'operator' => 'NOT IN',   // 除外する
        ),
    ),
) );

ステップ2:meta_queryでBETWEEN/LIKE/EXISTSを使う

<?php
/**
 * meta_query:カスタムフィールドの複合条件
 */

// === 例1:価格が1000〜5000円の商品(BETWEEN)===
$query = new WP_Query( array(
    'post_type'  => 'product',
    'meta_query' => array(
        array(
            'key'     => '_price',
            'value'   => array( 1000, 5000 ), // BETWEEN の場合は [最小値, 最大値]
            'type'    => 'NUMERIC',  // データ型: NUMERIC / DECIMAL / DATE / DATETIME / TIME / BINARY / CHAR
            'compare' => 'BETWEEN',
        ),
    ),
) );

// === 例2:タイトルに特定文字列を含む投稿(LIKE)===
$query2 = new WP_Query( array(
    'post_type'  => 'post',
    'meta_query' => array(
        array(
            'key'     => '_seo_title',
            'value'   => 'WordPress', // LIKE でワイルドカードは自動付与されない
            'compare' => 'LIKE',      // 部分一致
        ),
    ),
) );
// ※ LIKE は自動的に '%値%' にはならない。LIKEを使う場合は $wpdb->esc_like() を使うこと

// === 例3:メタキーが存在するかどうか(EXISTS / NOT EXISTS)===
$query3 = new WP_Query( array(
    'post_type'  => 'post',
    'meta_query' => array(
        'relation' => 'AND',
        // _featured フィールドが存在する投稿
        array(
            'key'     => '_featured',
            'compare' => 'EXISTS',
        ),
        // _expired フィールドが存在しない投稿
        array(
            'key'     => '_expired',
            'compare' => 'NOT EXISTS',
        ),
    ),
) );

// === 例4:日付範囲フィルタリング(DATE型)===
$query4 = new WP_Query( array(
    'post_type'  => 'event',
    'meta_query' => array(
        array(
            'key'     => '_event_date',
            'value'   => array( '2026-01-01', '2026-12-31' ),
            'type'    => 'DATE',
            'compare' => 'BETWEEN',
        ),
    ),
    'meta_key'   => '_event_date',
    'orderby'    => 'meta_value',
    'order'      => 'ASC',
) );

ステップ3:orderbよる複数フィールドとmeta_value_numで正確にソートする

<?php
/**
 * orderby:複数フィールドのソートと数値ソート
 */

// === 例1:複数フィールドでのソート(配列構文)===
// PHP 5.4以降・WordPress 4.0以降で使用可能
$query = new WP_Query( array(
    'post_type'  => 'product',
    'orderby'    => array(
        'meta_value_num' => 'ASC',  // 数値メタ値の昇順(価格)
        'title'          => 'ASC',  // 同じ価格なら名前の昇順
        'date'           => 'DESC', // 同じ名前なら新しい順
    ),
    'meta_key'   => '_price',       // meta_value_num / meta_value に必要
) );

// === 例2:meta_query のクローズを使ったソート ===
// named meta_query クローズを使うと meta_key が複数ある場合でもソート可能
$query2 = new WP_Query( array(
    'post_type'  => 'product',
    'meta_query' => array(
        'price_clause' => array(    // キー名(クローズ名)を定義
            'key'  => '_price',
            'type' => 'NUMERIC',
        ),
        'rating_clause' => array(
            'key'  => '_rating',
            'type' => 'NUMERIC',
        ),
    ),
    'orderby' => array(
        'rating_clause' => 'DESC', // 評価の高い順
        'price_clause'  => 'ASC',  // 同評価なら安い順
    ),
) );

// === 例3:ランダム順・関連度・メニュー順 ===
$query3 = new WP_Query( array(
    'post_type' => 'post',
    'orderby'   => array(
        'rand'       => 'ASC',   // ランダム(パフォーマンスに注意)
    ),
    'posts_per_page' => 5,
) );

// orderby に使えるその他の値
// 'ID', 'author', 'title', 'name', 'type', 'date', 'modified',
// 'parent', 'rand', 'comment_count', 'relevance', 'menu_order',
// 'meta_value', 'meta_value_num', 'post__in'(指定したID順)

ステップ4:no_found_rows・update_post_meta_cache・fields=’ids’でパフォーマンスを最適化する

<?php
/**
 * WP_Query のパフォーマンス最適化オプション
 */

// === 例1:ページネーション不要な場合の最適化 ===
// no_found_rows=true で SQL_CALC_FOUND_ROWS を省略(合計件数を計算しない)
// → get_the_posts_pagination() 等が使えなくなる代わりに高速化
$query = new WP_Query( array(
    'post_type'              => 'post',
    'posts_per_page'         => 10,
    'no_found_rows'          => true,  // ページネーション用のカウントをスキップ
    'update_post_meta_cache' => false, // ループ内で post_meta を使わない場合
    'update_post_term_cache' => false, // ループ内でタームデータを使わない場合
) );

// === 例2:IDのみ取得する(最も軽量)===
$post_ids = new WP_Query( array(
    'post_type'              => 'post',
    'posts_per_page'         => 100,
    'no_found_rows'          => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
    'fields'                 => 'ids', // IDの配列のみ取得(オブジェクトなし)
) );
// $post_ids->posts → [1, 2, 3, ...] の配列

// === 例3:メモリ効率の良いバッチ処理 ===
// 大量データを処理する場合は posts_per_page と paged を組み合わせる
function my_process_all_posts() {
    $page     = 1;
    $per_page = 100;

    do {
        $query = new WP_Query( array(
            'post_type'              => 'post',
            'posts_per_page'         => $per_page,
            'paged'                  => $page,
            'no_found_rows'          => false, // ループ終了判定に必要
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'fields'                 => 'ids',
        ) );

        if ( ! $query->have_posts() ) {
            break;
        }

        foreach ( $query->posts as $post_id ) {
            // 各投稿の処理
            my_process_single_post( $post_id );
        }

        // メモリを解放
        wp_reset_postdata();

        $page++;
        $has_more = $page <= $query->max_num_pages;

    } while ( $has_more );
}

ステップ5:posts_join/posts_where/posts_orderbyフィルターでSQLをカスタマイズする

<?php
/**
 * WP_Query の SQL を直接カスタマイズするフィルター
 * posts_join / posts_where / posts_orderby / posts_groupby / posts_fields
 *
 * 複数のクエリに影響しないよう、カスタムクエリ変数でフラグを管理する
 */

/**
 * 例:投稿のビュー数(カスタムテーブルに保存)で人気順ソートする
 */
class My_View_Count_Query {

    public static function init() {
        add_filter( 'posts_join',    array( __CLASS__, 'filter_join'    ), 10, 2 );
        add_filter( 'posts_where',   array( __CLASS__, 'filter_where'   ), 10, 2 );
        add_filter( 'posts_orderby', array( __CLASS__, 'filter_orderby' ), 10, 2 );
    }

    /**
     * カスタムテーブルをJOINする
     */
    public static function filter_join( $join, $query ) {
        // このクエリだけに適用するためのフラグを確認
        if ( ! $query->get( 'my_order_by_views' ) ) {
            return $join;
        }

        global $wpdb;
        // LEFT JOIN でビュー数テーブルを結合(ビュー数がない投稿も含める)
        $join .= " LEFT JOIN {$wpdb->prefix}my_post_views AS pv ON pv.post_id = {$wpdb->posts}.ID";

        return $join;
    }

    /**
     * WHERE 条件を追加する
     */
    public static function filter_where( $where, $query ) {
        if ( ! $query->get( 'my_order_by_views' ) ) {
            return $where;
        }

        $min_views = absint( $query->get( 'my_min_views' ) );
        if ( $min_views > 0 ) {
            global $wpdb;
            $where .= $wpdb->prepare( ' AND (pv.view_count >= %d OR pv.view_count IS NULL)', $min_views );
        }

        return $where;
    }

    /**
     * ORDER BY をビュー数に変更する
     */
    public static function filter_orderby( $orderby, $query ) {
        if ( ! $query->get( 'my_order_by_views' ) ) {
            return $orderby;
        }

        // COALESCE でNULL(ビュー数なし)を0として扱う
        return 'COALESCE(pv.view_count, 0) DESC, ' . $orderby;
    }
}

My_View_Count_Query::init();

// 使用例(カスタムフラグを設定してクエリを実行)
$popular_posts = new WP_Query( array(
    'post_type'          => 'post',
    'posts_per_page'     => 10,
    'no_found_rows'      => true,
    'my_order_by_views'  => true,  // フラグをON
    'my_min_views'       => 100,   // 100ビュー以上
) );

// クエリのデバッグ(生成されたSQLを確認する)
// define( 'SAVEQUERIES', true ); // wp-config.php で有効化
// echo $popular_posts->request; // 生成されたSQLを出力

注意事項

  • no_found_rowsとページネーション: no_found_rows=trueを設定するとmax_num_pagesが0になるため、the_posts_pagination()等が機能しません。ページネーションが必要なクエリには使用しないでください
  • meta_value_nummeta_key: orderby=>'meta_value_num'を使う場合は必ずmeta_keyパラメータを指定してください。指定がないとSQLエラーになります
  • フィルターのスコープ管理: posts_join等のフィルターはすべてのWP_Queryに影響するため、必ずカスタムクエリ変数でフラグを管理し、対象のクエリにのみ適用してください
  • tax_querymeta_queryrelation: 最上位のrelationtax_query/meta_query全体に適用されます。複雑な条件の場合はネスト構造を使ってグループを分けてください

まとめ

WP_Queryの高度な活用では、tax_queryrelationmeta_queryBETWEEN/EXISTS・複数フィールドのorderbyno_found_rows等の最適化パラメータ・posts_join/posts_whereフィルターを組み合わせることで、複雑な要件にも対応しながら高いパフォーマンスを維持できます。関連記事:WordPressプラグイン開発の基礎

お気軽にご相談ください

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