こんにちは。WordPressのコア内部構造を愛し、データベースのクエリ実行計画(EXPLAIN)を眺めるのが何よりの楽しみであるエンジニアです。
今日は、WordPressを使いこなしていく中で、誰もが一度はぶつかる「壁」についてお話ししましょう。それは、`WP_Query`における`tax_query`のパフォーマンス問題です。
特に、複数のタクソノミー(カテゴリーやタグなど)を「OR」で繋いで検索しようとしたとき、サイトの表示が急に重くなった経験はありませんか?
「コードは間違っていないはずなのに、なぜか遅い……」
その理由は、WordPressの内部で発行されるSQLの「結合(JOIN)」の仕組みにあります。ここを理解して制御できるようになれば、あなたはWordPress開発者として一段上のステージへ進むことができますよ。一緒に紐解いていきましょう。
—
1. なぜ「relation => OR」は重くなるのか?
まず、WordPressのデータベース構造を思い出してみましょう。記事(`posts`テーブル)とタクソノミー(`terms`テーブル)は、`term_relationships`という中間テーブルを介して紐付いています。
`WP_Query`で`tax_query`を使うと、WordPressは自動的にこれらのテーブルを結合(JOIN)するSQLを作成してくれます。
内部で起きていること(イメージ図)
例えば、「カテゴリーA」または「タグB」に属する記事を取得したい場合、WordPressは内部で以下のような動きをします。
1. `posts` テーブルに対して、`term_relationships` テーブルを2回以上JOINしようとする。
2. あるいは、巨大な `WHERE` 句の中に複雑なサブクエリを発行する。
— イメージ的なSQL(実際はもっと複雑です)
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
LEFT JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE (
term_taxonomy_id IN (10) — カテゴリーA
OR
term_taxonomy_id IN (25) — タグB
)
一見普通に見えますが、データ量が数万件を超えてくると、この「OR」条件がインデックスの効率的な利用を妨げ、データベースに大きな負荷をかける原因になります。特に、複数の異なるタクソノミーをまたぐ「OR」は、オプティマイザが最適な経路を見つけにくくなる「迷路」のようなものなのです。
—
2. 解決策その1:同一タクソノミーなら「IN」を使う
もしあなたが、「カテゴリーA」または「カテゴリーB」といった、同じタクソノミー内でのOR検索をしようとしているなら、`relation => OR` を使う必要はありません。
避けるべき書き方(冗長なパターン)
‘tax_query’ => array(
‘relation’ => ‘OR’, // これがJOINを複雑にする原因
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘news’,
),
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘blog’,
),
),
推奨される書き方(スマートなパターン)
`terms` を配列にして渡すだけで、内部的には効率的な `IN` 句が1つ発行されるだけになります。
‘tax_query’ => array(
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => array( ‘news’, ‘blog’ ), // 配列で渡すのが正解!
‘operator’ => ‘IN’,
),
),
—
3. 解決策その2:異なるタクソノミーの場合は「ID抽出」に分ける
実務で最も厄介なのが、「カテゴリーA(タクソノミー1)」または「タグB(タクソノミー2)」という、異なるタクソノミー間でのOR検索です。
これを無理やり1つの `WP_Query` で解決しようとすると、クエリが複雑化してパフォーマンスが極端に低下します。そこで、中級者以上の開発者がよく使うテクニックが、「クエリの分割と結合」です。
手順:
1. それぞれの条件で「投稿ID」だけをサクッと取得する(軽量なクエリ)。
2. 取得したIDをマージ(重複排除)する。
3. そのIDリストを使って、最終的な記事データを取得する。
これが実は、一発で巨大なクエリを投げるよりも遥かに高速なんです。
具体的で安全なコード例
/
- 異なるタクソノミーのOR検索を高速化するパターン
/
// 1. カテゴリー’news’の投稿IDを取得
$ids_news = get_posts(array(
‘post_type’ => ‘post’,
‘fields’ => ‘ids’, // IDだけを取得するのがポイント!(メモリ節約)
‘nopaging’ => true,
‘tax_query’ => array(
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘news’,
),
),
));
// 2. タグ’special’の投稿IDを取得
$ids_special = get_posts(array(
‘post_type’ => ‘post’,
‘fields’ => ‘ids’,
‘nopaging’ => true,
‘tax_query’ => array(
array(
‘taxonomy’ => ‘post_tag’,
‘field’ => ‘slug’,
‘terms’ => ‘special’,
),
),
));
// 3. IDを合体させて重複を消す
$merged_ids = array_unique( array_merge( $ids_news, $ids_special ) );
// 4. 最後に本番のWP_Queryを発行
if ( ! empty( $merged_ids ) ) {
$final_query = new WP_Query(array(
‘post_type’ => ‘post’,
‘post__in’ => $merged_ids, // プライマリキー(ID)での検索なので爆速!
‘orderby’ => ‘date’,
‘order’ => ‘DESC’,
));
// ここでループを開始
if ( $final_query->have_posts() ) {
while ( $final_query->have_posts() ) {
$final_query->the_post();
// 表示処理
}
}
wp_reset_postdata();
}
—
4. この手法がなぜ「最強」なのか
この手法が優れている理由は、「データベースの得意分野」を活かしているからです。
- インデックスの活用: `post__in` は、データベースの主キー(Primary Key)を直接狙い撃ちします。これはSQLにおいて最も高速な検索です。
- キャッシュの恩恵: 小さなクエリに分けることで、WordPressの内部キャッシュ(Object Cache)が効きやすくなります。
- メモリ効率: `fields => ids` を指定することで、余計な投稿本文などのデータをメモリに読み込まなくて済みます。
—
まとめ:WordPressを掌握する第一歩
「一つの関数(WP_Query)ですべてを解決しようとしないこと」。
これが、大規模なWordPressサイトを支えるエンジニアたちが大切にしている哲学です。
内部でどのようなSQLが発行され、データベースのインデックスがどう動くのか。それを少しだけ意識するだけで、あなたの書くコードの質は劇的に向上します。
今回の「OR検索の回避策」をマスターすれば、もう重いサイトに怯える必要はありません。複雑な仕様に出会ったときこそ、「どうすればクエリを単純化できるか?」をワクワクしながら考えてみてくださいね。
もし分からないことがあれば、いつでもコアのソースコード(`wp-includes/class-wp-query.php`)を覗いてみてください。そこには先人たちの知恵が詰まっていますよ。
一歩ずつ、楽しみながら学んでいきましょう!