【入門編】実務中級者向け:WP_Queryの「tax_query」における「relation => OR」が引き起こすクエリの複雑化と対策 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

こんにちは!WordPressの裏側の仕組みまでしっかりと理解して、ワンランク上のエンジニアを目指したいと思っていますか?

今回は、多くの実務中級者や、他のプログラミング言語からWordPressの世界へ飛び込んできた開発者が思わずハマってしまう「WP_Queryのダークサイド」についてお話しします。

テーマはずばり、`tax_query`における `relation => ‘OR’` が引き起こすパフォーマンスの悪化と、その回避・高速化アプローチです。

「条件に合致する投稿を広く取得したいだけなのに、なぜかサイトが重くなる…?」
そんな疑問を持ったことがあるなら、ここから先の内容はあなたの武器になりますよ。一緒にデータベースの深層を覗いていきましょう!

—

なぜ `tax_query` の `OR` はデータベースを泣かせるのか?

まずは、私たちが普段何気なく書いている `WP_Query` のコードを思い出してください。例えば、「ニュース」または「イベント」という2つのカテゴリ(タクソノミー)のいずれかに属する投稿を取得したい時、次のようなコードを書きますよね。

$args = array(
‘post_type’ => ‘post’,
‘posts_per_page’ => 10,
‘tax_query’ => array(
‘relation’ => ‘OR’,
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘news’,
),
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘event’,
),
),
);

$query = new WP_Query( $args );

一見、とても直感的で分かりやすいコードです。ですが、ここからWordPressの内部コア(`WP_Query` クラス)が裏側で発行するSQLクエリを想像したことはありますか?

実は、この `relation => ‘OR’` を指定した瞬間、WordPressはリレーショナルデータベース(MySQL)にとって非常に都合の悪いSQLを組み立ててしまうのです。

裏側で発行されるSQLのイメージと「インデックスの消滅」

データベース(MySQL)は、適切にインデックス(索引)が貼られていれば、爆速でデータを探し出すことができます。しかし、`tax_query` で `OR` を使うと、内部的に以下のような複雑な結合(JOIN)や副問合せ(サブクエリ)、そして悪名高い `OR` 条件を含んだWHERE句が生成されます。

— 概念的なイメージ(実際はもっと複雑なJOINと子クエリが走ります)
SELECT FROM wp_posts
INNER JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE (
wp_term_relationships.term_taxonomy_id = 5
OR
wp_term_relationships.term_taxonomy_id = 8
)
AND wp_posts.post_type = ‘post’
GROUP BY wp_posts.ID;

ここで何が起きているかというと、MySQLのオプティマイザ(実行計画を立てる頭脳)が「あ、このクエリは効率的なインデックスを使えないな。全件スキャン(テーブル全体を上から順になめる)するしかないか…」と判断してしまうのです。

投稿数が数万件程度であれば体感できないかもしれませんが、数十万件、数百万件とスケールした途端、このクエリはCPUを急上昇させ、データベースの接続数を食い潰すボトルネックに変貌します。これが「`OR` が引き起こすパフォーマンスの悪化」の正体です。

—

サブクエリの呪縛から逃れる:UNIONアプローチ

では、このパフォーマンスの悪化を防ぐにはどうすれば良いのでしょうか?
ここで私たちが取るべきスマートなアプローチの一つが、「複数の軽量なクエリに分割し、結果を合算する(UNIONの思想)」という方法です。

`WP_Query` で無理やり一つの重いSQLを発行させるのではなく、「newsの投稿ID配列」と「eventの投稿ID配列」を個別に取得し、PHP側(あるいはSQLの `UNION`)で統合するのです。

特に、それぞれの条件が個別のインデックスを綺麗に使える形であれば、MySQLはそれぞれの検索を瞬時に終わらせてくれます。

実装例:個別クエリの結合による高速化

例えば、次のように書くことで、データベースの負荷を劇的に下げることができます。

// 1つ目の条件(news)に特化した高速なクエリ
$query_news = new WP_Query( array(
‘post_type’ => ‘post’,
‘posts_per_page’ => 5,
‘fields’ => ‘ids’, // 投稿IDだけを取得して軽量化!
‘tax_query’ => array(
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘news’,
),
),
) );

// 2つ目の条件(event)に特化した高速なクエリ
$query_event = new WP_Query( array(
‘post_type’ => ‘post’,
‘posts_per_page’ => 5,
‘fields’ => ‘ids’, // 投稿IDだけを取得
‘tax_query’ => array(
array(
‘taxonomy’ => ‘category’,
‘field’ => ‘slug’,
‘terms’ => ‘event’,
),
),
) );

// 2つの結果をPHP側で合算して重複を排除
$post_ids = array_unique( array_merge( $query_news->posts, $query_event->posts ) );

// 合算したIDを元に、最終的な表示用クエリを実行(これが一番効率的!)
if ( ! empty( $post_ids ) ) {
$final_query = new WP_Query( array(
‘post_type’ => ‘post’,
‘post__in’ => $post_ids,
‘orderby’ => ‘post__in’, // IDの順序を維持
‘posts_per_page’ => 5,
) );

// ループ処理へ…
}

「おや、クエリを2回(あるいはそれ以上)投げる方が非効率なのでは?」と思うかもしれませんが、インデックスが効かない巨大なテーブルをスキャンするコストに比べれば、インデックスが完璧に効く単一条件のクエリを数回実行する方が、データベース全体の負荷は圧倒的に低くなるのです。これがデータベースチューニングの妙味ですね。

—

もう一つの究極の選択肢:メタ/タクソノミー構造の再設計

もし、あなたのプロジェクトがこれから設計段階である、あるいは大規模メディアサイトのパフォーマンス改善を任されているのであれば、もっと根本的な解決策があります。

それが、「そもそもWordPressの標準タクソノミー構造に頼りすぎない」というアプローチです。

WordPressのデフォルトの構造(`wp_term_relationships` テーブルなど)は汎用性が高い反面、多重のJOINが必須になる構造上の宿命を背負っています。ここで極限のパフォーマンスを追求するシニアエンジニアたちは、次のような設計上の工夫を取り入れます。

1. カスタムフィールド(Post Meta)やカスタムカラムの活用
もし条件が「AまたはB」とシンプルで、かつデータ構造がフラットに保てるのであれば、タクソノミーではなく `wp_postmeta` にフラグを持たせたり、`wp_posts` テーブル自体にカスタムカラムを追加してインデックスを貼ったりする方が、検索クエリが圧倒的にシンプルになります。
2. オブジェクトキャッシュ(Redis / Memcached)の徹底活用
ここまで最適化してもなお発生するデータベースへのアクセスは、外部キャッシュ層で完全にヒットさせます。トランジェントAPIやオブジェクトキャッシュを組み合わせることで、データベースに到達するリクエスト数そのものをゼロに近づけるのが王道です。

—

ここをクリアすれば、あなたは単なる「WordPressの使い方を知っている人」から、「WordPressの内部構造をコントロールして高速なシステムを構築できるエンジニア」へとステップアップできます。

複雑なクエリに出会ったときは、「この条件の裏で、MySQLはどのインデックスを使えているだろう?」と想像する癖をつけてみてくださいね。
日々の開発が、もっとエキサイティングで楽しいものになりますよ!

タイトルとURLをコピーしました