2026年7月1日

2026年7月1日

WP_Queryの高度な使い方|tax_query・meta_queryの複合条件、並び替え、高速化、SQLフィルター

WP_Query は、WordPress で投稿を取り出すときの中心になるクラスです。基本の post_type や posts_per_page だけでなく、タクソノミーとカスタムフィールドの複合条件、複数の項目での並び替え、件数計算やキャッシュの省略による高速化、SQL を直接書き換えるフィルターまで用意されています。

この記事では、それぞれの使い方と、誤解されやすい点(LIKE のワイルドカード、meta_value_num と meta_key の関係、wp_reset_postdata() の役割など)を、WordPress 本体のコード(class-wp-query.php・class-wp-meta-query.php)で確かめた内容に沿って説明します(2026年10月時点)。

tax_query で複数のタクソノミー条件を組み合わせる

tax_query は、カテゴリー・タグ・カスタムタクソノミーの条件を配列で並べ、relation(AND または OR)でつなぎます。条件の配列の中にさらに relation 付きの配列を入れると、入れ子のグループを作れます。

<?php
// 例1: カテゴリー「technology」に属し、かつタグ「wordpress」か「php」が付いた投稿
$query = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		'relation' => 'AND',
		array(
			'taxonomy' => 'category',
			'field'    => 'slug',      // 'term_id'(既定)/ 'name' / 'slug' / 'term_taxonomy_id'
			'terms'    => 'technology',
		),
		array(
			'taxonomy' => 'post_tag',
			'field'    => 'slug',
			'terms'    => array( 'wordpress', 'php' ),
			'operator' => 'IN',        // 'IN'(既定)/ 'NOT IN' / 'AND' / 'EXISTS' / 'NOT EXISTS'
		),
	),
) );

// 例2: (カテゴリー「news」または「tutorial」)かつ タグ「featured」
$query2 = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		'relation' => 'AND',
		array(
			'relation' => 'OR',
			array(
				'taxonomy' => 'category',
				'field'    => 'slug',
				'terms'    => 'news',
			),
			array(
				'taxonomy' => 'category',
				'field'    => 'slug',
				'terms'    => 'tutorial',
			),
		),
		array(
			'taxonomy' => 'post_tag',
			'field'    => 'slug',
			'terms'    => 'featured',
		),
	),
) );

// 例3: 指定したカテゴリーに属さない投稿
$query3 = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		array(
			'taxonomy' => 'category',
			'field'    => 'slug',
			'terms'    => array( 'uncategorized', 'test' ),
			'operator' => 'NOT IN',
		),
	),
) );

relation はその階層の条件どうしのつなぎ方だけを決めます。上の階層と下の階層で別の relation を使えるので、「A または B、かつ C」のような条件は例2のように入れ子で書きます。

meta_query でカスタムフィールドを絞り込む

meta_query は、カスタムフィールド(投稿メタ)の条件を指定します。主な指定は次のとおりです。

キー 指定できる値
compare =、!=、>、>=、<、<=、LIKE、NOT LIKE、IN、NOT IN、BETWEEN、NOT BETWEEN、REGEXP、NOT REGEXP、RLIKE、EXISTS、NOT EXISTS
type NUMERIC、BINARY、CHAR(既定)、DATE、DATETIME、DECIMAL、SIGNED、TIME、UNSIGNED

メタの値はデータベースでは文字列として保存されているため、数値や日付として比べるときは type を指定します。指定しないと CHAR として比べるので、'10' が '9' より小さいと判定されるといったずれが起きます。

<?php
// 例1: 価格が 1000〜5000 の商品(数値として比較)
$query = new WP_Query( array(
	'post_type'  => 'product',
	'meta_query' => array(
		array(
			'key'     => '_price',
			'value'   => array( 1000, 5000 ), // BETWEEN は [最小値, 最大値]
			'type'    => 'NUMERIC',
			'compare' => 'BETWEEN',
		),
	),
) );

// 例2: メタの値に「WordPress」を含む投稿(部分一致)
$query2 = new WP_Query( array(
	'post_type'  => 'post',
	'meta_query' => array(
		array(
			'key'     => '_seo_title',
			'value'   => 'WordPress', // % を付けずに渡す
			'compare' => 'LIKE',
		),
	),
) );

// 例3: _featured があり、_expired が無い投稿
$query3 = new WP_Query( array(
	'post_type'  => 'post',
	'meta_query' => array(
		'relation' => 'AND',
		array(
			'key'     => '_featured',
			'compare' => 'EXISTS',
		),
		array(
			'key'     => '_expired',
			'compare' => 'NOT EXISTS',
		),
	),
) );

// 例4: 2026年中のイベントを開催日の昇順で
$query4 = new WP_Query( array(
	'post_type'  => 'event',
	'meta_query' => array(
		'event_date' => array(
			'key'     => '_event_date',
			'value'   => array( '2026-01-01', '2026-12-31' ),
			'type'    => 'DATE',
			'compare' => 'BETWEEN',
		),
	),
	'orderby'    => 'event_date', // 上の句の名前で並び替える
	'order'      => 'ASC',
) );

LIKE は自動で前後に % が付く

compare が LIKE または NOT LIKE のとき、WordPress は値を $wpdb->esc_like() でエスケープしたうえで、前後に % を付けて SQL を組み立てます。つまり例2は LIKE '%WordPress%' という部分一致になります。

このため、値に自分で % を付ける必要はありません。付けた % はエスケープされて文字としての「%」を探す条件になり、意図どおりに一致しなくなります。前方一致('WordPress%')のように % の位置を自分で決めたい場合は、LIKE ではなく REGEXP(例: '^WordPress')を使います。

日付の比較は保存形式に合わせる

type に DATE を指定すると、値は CAST(meta_value AS DATE) で比べられます。メタの値が 2026-03-15 のような YYYY-MM-DD 形式で保存されていれば例4のとおり動きます。ACF の日付フィールドのように 20260315 形式で保存するものもあるので、実際の保存形式を確認してから条件を書いてください。

並び替え(orderby)を複数の項目で行う

orderby に配列を渡すと、複数の項目で順に並び替えられます。

<?php
// 例1: 価格の安い順、同じ価格ならタイトル順
$query = new WP_Query( array(
	'post_type' => 'product',
	'meta_key'  => '_price',
	'orderby'   => array(
		'meta_value_num' => 'ASC',
		'title'          => 'ASC',
	),
) );

// 例2: 名前付きの meta_query 句で、2つのメタを使って並び替える
$query2 = new WP_Query( array(
	'post_type'  => 'product',
	'meta_query' => array(
		'rating_clause' => array(
			'key'  => '_rating',
			'type' => 'NUMERIC',
		),
		'price_clause'  => array(
			'key'  => '_price',
			'type' => 'NUMERIC',
		),
	),
	'orderby'    => array(
		'rating_clause' => 'DESC', // 評価の高い順
		'price_clause'  => 'ASC',  // 同じ評価なら安い順
	),
) );

並び替えに使うメタの扱いで、注意する点が2つあります。

  • meta_value と meta_value_num は、meta_key か meta_query でメタの条件が指定されているときだけ有効です。指定が無い場合、エラーにはならず、その並び替え指定が無視されます(ほかの指定も残らなければ投稿日順になります)。並びが変わらないときは、まずここを確認してください。
  • meta_key を指定すると、そのメタを持たない投稿は結果から外れます。メタが無い投稿も含めたい場合は、名前付き句の側で 'compare' => 'EXISTS' と 'NOT EXISTS' を OR でつなぐなどの工夫が要ります。

orderby にはほかに none、ID、author、title、name、type、date、modified、parent、rand、comment_count、menu_order、post__in(post__in で渡したIDの順)などを指定できます。relevance(検索の関連度順)は、検索語 s を指定したときだけ意味を持ちます。rand は件数が多いと重く、後述のクエリキャッシュも効かないので、多用は避けます。

パフォーマンスを上げるパラメータ

一覧の件数やページ送りが要らない場面、IDだけ欲しい場面では、次のパラメータで処理を減らせます。

パラメータ 効果 使わないほうがよい場面
'no_found_rows' => true 総件数の計算(SQL_CALC_FOUND_ROWS と SELECT FOUND_ROWS())を省く ページ送りを表示する一覧
'update_post_meta_cache' => false 取得した投稿のメタをまとめて読み込む処理を省く ループ内で get_post_meta() を使う
'update_post_term_cache' => false 取得した投稿のタームをまとめて読み込む処理を省く ループ内でカテゴリーやタグを表示する
'fields' => 'ids' 投稿オブジェクトを作らず、IDの配列だけを返す タイトルや本文を表示する
<?php
// サイドバーの「新着5件」のように、ページ送りが不要な一覧
$recent = new WP_Query( array(
	'post_type'              => 'post',
	'posts_per_page'         => 5,
	'no_found_rows'          => true,
	'update_post_meta_cache' => false,
	'update_post_term_cache' => false,
	'ignore_sticky_posts'    => true,
) );

// IDだけが欲しい場合($ids_query->posts が ID の配列になる)
$ids_query = new WP_Query( array(
	'post_type'      => 'post',
	'posts_per_page' => 100,
	'no_found_rows'  => true,
	'fields'         => 'ids',
) );

no_found_rows を true にすると、found_posts と max_num_pages が計算されず 0 のままになります。そのため the_posts_pagination() などのページ送りは表示されません。

WordPress 6.1 以降、cache_results(既定は true)が有効なら、WP_Query は投稿IDを取り出すクエリの結果をオブジェクトキャッシュに保存します。Redis などの永続オブジェクトキャッシュを入れているサイトでは、同じ条件のクエリが次のリクエストでもキャッシュから返ります。並び順に rand を含むクエリはキャッシュの対象外です。

大量の投稿を少しずつ処理する

全投稿に何かの処理をするときは、一度に全件を読み込まず、件数を区切って順に取り出します。

<?php
function my_process_all_posts() {
	$page     = 1;
	$per_page = 100;

	do {
		$query = new WP_Query( array(
			'post_type'              => 'post',
			'post_status'            => 'any',
			'posts_per_page'         => $per_page,
			'paged'                  => $page,
			'orderby'                => 'ID',  // 並びを固定して取りこぼしを防ぐ
			'order'                  => 'ASC',
			'fields'                 => 'ids',
			'no_found_rows'          => true,  // 総件数は使わない
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		) );

		foreach ( $query->posts as $post_id ) {
			my_process_single_post( $post_id ); // 1件ごとの処理(自作)
		}

		$page++;
	} while ( count( $query->posts ) === $per_page ); // 取得数が満たなければ最後のページ
}

総件数を使わず、「取得した件数が $per_page に満たなければ終わり」と判定しているので、no_found_rows を true にできます。処理の中で投稿を削除したり条件から外れる変更をしたりすると、ページの位置がずれて取りこぼしが出るため、その場合は毎回 paged を 1 のまま取り直す書き方にします。

wp_reset_postdata() の役割

wp_reset_postdata() は、メモリを解放する関数ではありません。サブクエリのループで $query->the_post() を呼ぶと、グローバル変数 $post がサブクエリの投稿に書き換わります。wp_reset_postdata() は、この $post をメインクエリの現在の投稿に戻し、the_title() などのテンプレートタグがメインの投稿を指す状態に戻します。

<?php
$related = new WP_Query( array(
	'post_type'      => 'post',
	'posts_per_page' => 3,
	'no_found_rows'  => true,
) );

if ( $related->have_posts() ) {
	echo '<ul>';
	while ( $related->have_posts() ) {
		$related->the_post(); // ここでグローバルの $post が書き換わる
		printf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink() ), esc_html( get_the_title() ) );
	}
	echo '</ul>';
	wp_reset_postdata(); // $post をメインクエリの投稿に戻す
}

前の節の一括処理のように the_post() を呼ばず、$query->posts を直接回すだけなら、wp_reset_postdata() は不要です。また、query_posts() でメインクエリ自体を差し替えたときに使うのは wp_reset_query() です(query_posts() 自体、使わないことが推奨されています)。

posts_join・posts_where・posts_orderby で SQL を書き換える

パラメータだけでは表せない条件は、SQL の各部分を受け取るフィルターで書き換えます。どのフィルターも2番目の引数で WP_Query のインスタンスを受け取れるので、独自のクエリ変数を目印にして、対象のクエリにだけ効くようにします。

次の例は、閲覧数を保存した独自テーブル {prefix}my_post_views(post_id・view_count の列を持ち、1投稿1行)を結合して、閲覧数の多い順に並べます。

<?php
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 );
	}

	public static function filter_join( $join, $query ) {
		if ( ! $query->get( 'my_order_by_views' ) ) {
			return $join;
		}
		global $wpdb;
		$join .= " LEFT JOIN {$wpdb->prefix}my_post_views AS pv ON pv.post_id = {$wpdb->posts}.ID";
		return $join;
	}

	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', $min_views );
		}
		return $where;
	}

	public static function filter_orderby( $orderby, $query ) {
		if ( ! $query->get( 'my_order_by_views' ) ) {
			return $orderby;
		}
		// 閲覧数の記録が無い投稿は 0 として扱う
		return 'COALESCE(pv.view_count, 0) DESC, ' . $orderby;
	}
}
My_View_Count_Query::init();

// 使用例
$popular = new WP_Query( array(
	'post_type'         => 'post',
	'posts_per_page'    => 10,
	'no_found_rows'     => true,
	'my_order_by_views' => true, // 独自のクエリ変数(目印)
	'my_min_views'      => 100,  // 100回以上閲覧された投稿だけ
) );

WP_Query に渡した独自のキーはクエリ変数として保持されるので、フィルターの中で $query->get() で読めます。書くときの注意は次のとおりです。

  • 目印を確かめずにフィルターを付けると、管理画面の一覧やメインクエリを含むすべての WP_Query に効いてしまいます。
  • get_posts() は既定で suppress_filters を true にするため、posts_join や posts_where が呼ばれません。このフィルターを使うときは WP_Query を使うか、get_posts() に 'suppress_filters' => false を渡します。
  • 結合するテーブルが1投稿に複数行を持つ場合は、同じ投稿が重複して返ります。その場合は posts_groupby フィルターで {$wpdb->posts}.ID を指定するか、集計済みのテーブルを結合します。
  • 外から受け取った値を SQL に入れるときは、例のように $wpdb->prepare() か absint() などで整えてから使います。

生成された SQL を確認する

WP_Query が実行した SQL は、$query->request に入っています。SAVEQUERIES を有効にしなくても読めます。

<?php
// 開発環境で一時的に確認する
error_log( $popular->request );

SAVEQUERIES は、ページ内で実行されたすべてのクエリと所要時間を $wpdb->queries に記録する設定で、wp-config.php に define( 'SAVEQUERIES', true ); を書いて有効にします。記録の分だけ処理が重くなるので、本番環境では有効にしたままにしないでください。どのクエリが遅いかを画面で確かめたいときは、Query Monitor プラグインも便利です。

まとめ

  • tax_query・meta_query は relation を入れ子にして、「A または B、かつ C」のような条件を作れる。
  • meta_query の LIKE は前後に % が自動で付く。値に % を付けない。
  • meta_value_num での並び替えは、meta_key か meta_query が無いと無視される。複数のメタで並べるときは名前付きの句を使う。
  • ページ送りが要らない一覧では no_found_rows、IDだけで足りる処理では fields => 'ids' を使う。
  • wp_reset_postdata() は the_post() で書き換わった $post を戻す関数で、メモリの解放とは関係ない。
  • SQL フィルターは独自のクエリ変数で対象を絞り、生成された SQL は $query->request で確認する。

フィルターを自作プラグインにまとめる方法は、WordPressプラグイン開発の基礎で説明しています。

お気軽にご相談ください

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