WordPressの「深淵」を制御する:`term_relationships`を回避したクエリの再構築
WordPressにおけるスケーラビリティの最大のボトルネックは、多くの者が認める通り、EAV(Entity-Attribute-Value)モデルの弊害である。特に `term_relationships` テーブルの行数が数百万単位を超えた瞬間、`WP_Query` のデフォルト挙動は、インデックスが十分に効いていたとしても、MySQLの結合(JOIN)コストという名の「死の淵」へと我々を誘う。
本稿では、一般的なクエリ最適化の枠を超え、WordPressの内部構造をハックすることで、この制約を物理的に回避する手法を解説する。
—
1. なぜ `term_relationships` はスケーラビリティを阻害するのか
`WP_Query` がタクソノミーを指定して投稿を取得する際、内部では `get_posts()` が実行され、最終的に `WP_Tax_Query` が SQL を生成する。ここで発行される `INNER JOIN` は、`term_taxonomy` との結合を伴い、データセットが肥大化すればするほど、MySQLのオプティマイザは中間結果セット(Temporary Table)の生成にメモリを浪費する。
もしあなたが「特定のタクソノミーを持つ最新の投稿を10件取得する」という単純な処理で、`JOIN` を許容しているならば、それはアーキテクチャの敗北である。
—
2. 禁断の果実:`posts_clauses` による JOIN 排除
クエリを最適化する最も即効性のある手段は、`WP_Query` の実行過程でフックを差し込み、`JOIN` を `EXISTS` または「メタ情報のフラット化」に置き換えることだ。
以下のコードは、`term_relationships` を介さず、タクソノミー情報を `wp_postmeta` または専用の検索用カスタムテーブルへオフロードしている前提の最適化パターンである。
/
- WP_Query の SQL 生成プロセスに介入し、コストの高い JOIN を排除する
/
add_filter(‘posts_clauses’, function($clauses, $query) {
global $wpdb;
// 特定の条件下でのみ実行(パフォーマンスのためのガード)
if (!$query->get(‘fast_taxonomy_query’)) {
return $clauses;
}
// 既存のJOIN句から term_relationships を物理的に除去
// 正規表現で JOIN を置換し、サブクエリでインデックスを強制する戦略
$clauses[‘join’] = preg_replace(
“/INNER JOIN {$wpdb->term_relationships}.?ON \(.?\)/”,
“”,
$clauses[‘join’]
);
// 必要に応じて WHERE 句を EXISTS 句へ書き換え、全件走査を防止する
// これにより、MySQLは該当レコードを見つけた時点で走査を打ち切る
$clauses[‘where’] .= ” AND EXISTS (
SELECT 1 FROM {$wpdb->term_relationships} tr
WHERE tr.object_id = {$wpdb->posts}.ID
AND tr.term_taxonomy_id = 42
)”;
return $clauses;
}, 10, 2);
—
3. インデックスチューニング:カーディナリティの極限利用
`EXISTS` を使う場合、`term_relationships` テーブルの `(term_taxonomy_id, object_id)` という複合インデックスが適切に張られていることが絶対条件だ。
ここでシニアエンジニアが意識すべきは、「カバリングインデックス」である。MySQLの実行計画を確認し、`Using index` が表示されているかを確認せよ。もし `Using where; Using index` となっていなければ、クエリはインデックスツリーから実際のデータ行(ヒープ)にアクセスしており、IO待ちが発生している。
— 推奨されるインデックス構成
CREATE INDEX idx_taxonomy_optimized ON wp_term_relationships (term_taxonomy_id, object_id);
—
4. キャッシュ戦略:クエリそのものを「メモ化」するな
多くの開発者が行う `get_transient` によるクエリ結果のキャッシュは、データ整合性の観点で危険である。我々が目指すべきは、「クエリ発行のオーバーヘッドそのものを消し去る」ことだ。
`WP_Query` を実行する前に、Redis等のオブジェクトキャッシュを用いて「term_id -> post_ids」のマップを保持せよ。
// データベースへのクエリ発行をバイパスする、真の高速化パターン
$term_id = 42;
$cache_key = “term_posts_{$term_id}”;
$post_ids = wp_cache_get($cache_key, ‘custom_group’);
if (false === $post_ids) {
// データベースから必要なIDのみを取得(カラム数を絞る)
$post_ids = $wpdb->get_col($wpdb->prepare(
“SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d”,
$term_id
));
wp_cache_set($cache_key, $post_ids, ‘custom_group’, HOUR_IN_SECONDS);
}
// WP_Query には post__in を渡すだけ。JOINは一切発生しない。
$query = new WP_Query([‘post__in’ => $post_ids, ‘post_type’ => ‘post’]);
—
5. 結論:制御下に置くこと
WordPressは「誰でも使える」CMSであるが、それは「誰でも最適化できる」ことを意味しない。`term_relationships` の巨大化は、単なるDBの問題ではなく、アーキテクチャ設計の怠慢である。
- JOINを疑え: SQLの結合は、データ量に対して非線形にコストが増加する。
- サブクエリを制御せよ: `EXISTS` は、適切にインデックスが張られていれば `JOIN` よりも圧倒的に高速である。
- メモリとIOのバランス: アプリケーションレイヤーでのIDキャッシュこそが、最終的な勝負所となる。
この知見を胸に、自身のシステムの実行計画(`EXPLAIN`)を叩き直せ。ボトルネックは、常に「知ろうとしない者の前」にだけ存在しているのだから。