WP_Queryの内部発行クエリ最適化:`post_parent`検索を極限まで加速するインデックス設計
大規模な階層構造(Hierarchical Post Types)を持つWordPressシステムにおいて、`’post_parent’ => $id` を指定した `WP_Query` は、しばしばデータベース層のパフォーマンスボトルネックとなる。
一般的な開発者は、`WP_Query` にパラメータを渡すことで満足しがちだが、シニアエンジニアであれば、その背後でMySQL(またはMariaDB)のストレージエンジンがどのように動作し、オプティマイザがどのような実行計画(Execution Plan)を選択しているかに目を向けなければならない。
本稿では、`wp_posts` テーブルの内部構造とB-Treeインデックスの数学的特性を紐解き、数百万件規模のレコードに対してもミリ秒単位で応答するインデックスチューニングの極意を解説する。
—
1. `WP_Query` が発行するクエリの解剖
まずは、私たちが何気なく書いている以下のコードが、MySQLに対してどのようなSQLを発行しているのかを正確に把握する。
$query = new WP_Query([
‘post_type’ => ‘page’,
‘post_parent’ => 12345,
‘posts_per_page’ => 10,
]);
このリクエストにより、WordPressコアの `WP_Query::get_posts()` は、おおむね以下のようなSQLを生成する(キャッシュやフィルターが介入しない場合のプリミティブな構造)。
SELECT SQL_CALC_FOUND_ROWS wp_posts.
FROM wp_posts
WHERE 1=1
AND wp_posts.post_parent = 12345
AND wp_posts.post_type = ‘page’
AND (wp_posts.post_status = ‘publish’ OR wp_posts.post_status = ‘private’)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
ここで注目すべきは、`SQL_CALC_FOUND_ROWS` の存在、そして `WHERE` 句における複数のカラム条件と `ORDER BY` の組み合わせである。
デフォルトのインデックス構成の限界
標準のWordPressインストールでは、`wp_posts` テーブルには以下のインデックスが存在する。
- `PRIMARY KEY (`ID`)`
- `KEY `post_name` (`post_name`(191))`
- `KEY `type_status_date` (`post_type`, `post_status`, `post_date`, `ID`)`
- `KEY `post_parent` (`post_parent`)`
一見、`post_parent` には単体インデックスが貼られているため問題ないように見える。しかし、これは致命的な誤解である。
MySQLのクエリパーサとオプティマイザは、基本的には 1つのクエリにつき1つのインデックスしか使用しない(Index Mergeが発生しない限り)。
`WHERE post_parent = 12345 AND post_type = ‘page’` という条件がある場合、MySQLはどちらのインデックスを使うべきか迷うか、あるいはカーディナリティ(値の分散度)の統計情報に基づいて非効率なスキャンを選択する。
特に `post_parent` は値の重複が多く、単体インデックスでは絞り込み効率(Selectivity)が非常に低い。結果として、ファイルソート(Using filesort)や全件スキャン(Full Table Scan)に近い挙動を引き起こす。
—
2. 複合インデックス(Composite Index)の設計理論
この問題を解決するには、MySQLのストレージエンジン(InnoDB)のB-Tree構造をハックするための複合インデックスを明示的に構築する必要がある。
インデックスを設計する際の黄金律は以下の通りである。
1. 等価比較(Equality)を行うカラムを左側に配置する。
2. 範囲検索(Range)やソート(ORDER BY)に関与するカラムを右側に配置する。
今回のクエリ要件をこの原則に当てはめると、以下のカラム群が対象となる。
- `post_type` (等価)
- `post_parent` (等価)
- `post_status` (等価、またはIN句による複数値)
- `post_date` (ソート)
最適な複合インデックスの定義
データベース管理者(DBA)として、以下のDDLを本番環境の `wp_posts` テーブルに適用する。
ALTER TABLE wp_posts
ADD INDEX idx_parent_optimization (post_type, post_parent, post_status, post_date);
このインデックスがなぜ極限まで最適化されているのか、InnoDBのB-Treeのリーフノードの並び順をシミュレートしてみよう。
1. まず `post_type`(例: ‘page’)で完全にグループ化される。
2. その同一 `post_type` の中で、さらに `post_parent`(例: 12345)の値順に綺麗に整列される。
3. さらにその中で `post_status` ごとに整理され、最終的にリーフノード自体が `post_date` の降順/昇順に親切に並べられた状態でメモリ上にキャッシュされる。
これにより、MySQLはディスクシークをほぼ発生させことなく、メモリ上のB-Treeを辿るだけで該当レコードを一網打尽に取得し、さらに `ORDER BY` のためのファイルソートを完全にバイパス(回避)できるようになる。
—
3. `SQL_CALC_FOUND_ROWS` という最大の障壁
インデックスチューニングを語る上で避けて通れないのが、WordPressがデフォルトで実行する `SQL_CALC_FOUND_ROWS` である。
この修飾子がクエリに含まれている場合、MySQLは `LIMIT` 句を無視して条件に合致するすべてのレコードをスキャンし、総数を計算することを強制される。つまり、どんなに完璧なインデックスを貼っても、`SQL_CALC_FOUND_ROWS` が存在するかぎり、MySQLは巨大な結果セットのカウンティング処理を免れない。
大規模サイトや高負荷なシステムでは、この挙動は致命的である。ページネーションの総数表示が厳密である必要がないのであれば、コード側で `no_found_rows => true` を指定し、この悪名高いオーバーヘッドを根絶しなければならない。
実務で使うべき最適化された WP_Query 実装例
以下に、インデックスの恩恵を100%引き出し、メモリ消費とレイテンシを極限まで削ぎ落とした実装パターンを示す。
/
- 階層構造を持つ投稿の親ID検索を極限まで高速化するラッパー関数
- @param int $parent_id 親ポストID
- @param int $paged ページ番号
- @param int $per_page 1ページあたりの件数
- @return WP_Query
/
function get_optimized_child_posts( int $parent_id, int $paged = 1, int $per_page = 10 ): WP_Query {
$args = [
‘post_type’ => ‘page’,
‘post_parent’ => $parent_id,
‘posts_per_page’ => $per_page,
‘paged’ => $paged,
// 【重要】SQL_CALC_FOUND_ROWS を無効化し、クエリ実行速度を倍化させる
‘no_found_rows’ => true,
// 不要なメタデータのキャッシュロードを抑制
‘update_post_meta_cache’ => false,
// 必要最小限のタームキャッシュのみに絞る
‘update_post_term_cache’ => false,
‘orderby’ => ‘post_date’,
‘order’ => ‘DESC’,
// オプティマイザに意図しないキャッシュ汚染を防ぐためのヒント
‘cache_results’ => true,
];
$query = new WP_Query( $args );
return $query;
}
—
4. パフォーマンス検証:EXPLAIN による実証
アーキテクトとして、感覚ではなく数学的・物理的な証拠を提示しよう。
上記のインデックス(`idx_parent_optimization`)を付与した状態で、`EXPLAIN` 構文を用いてクエリの実行計画を確認する。
EXPLAIN
SELECT wp_posts.
FROM wp_posts
WHERE wp_posts.post_type = ‘page’
AND wp_posts.post_parent = 12345
AND (wp_posts.post_status = ‘publish’)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
チューニング前の実行計画(悪夢)
- type: `ref` または `index`
- key: `post_parent`
- Rows examined: 数万〜数十万行
- Extra: `Using where; Using filesort` (最悪の組み合わせ。ディスクI/OとCPUを大量消費する)
チューニング後の実行計画(極限の美しさ)
- type: `ref` (または `range`)
- key: `idx_parent_optimization`
- Rows examined: 該当する子ポストの数(数件〜数十件のみ)
- Extra: `Using index condition` (あるいは余計なメッセージが一切ないクリーンな状態。`Using filesort` が完全に消滅している)
`Using filesort` が消え去るということは、MySQLが一時テーブル(Temporary Table)やファイルシステムへの書き出しを行わず、すべてRAM上のバッファプール内でソート済みの結果を即座に返却していることを意味する。
—
結びにかえて
WordPressは「誰でも簡単に使えるCMS」という優しい顔の裏に、数百万件のレコードを飲み込む巨大なリレーショナルデータベースを隠し持っている。
プラグインやテーマをただ組み込むだけの開発から脱却し、データベースエンジンの物理層、B-Treeインデックスの挙動、そしてランタイムのメモリ効率までを完全にコントロール下に置くこと。それこそが、真の意味で「WordPressを掌握する」ということである。
サーバーのリソースは有限であり、システムのスケーラビリティは常にこうした泥臭いインデックスの1行、クエリの1つのパラメータから始まる。本稿で示したアプローチを直ちに検証環境へ投入し、その圧倒的な応答速度の差を体感してほしい。