【実務・中級編】実務中級者向け:WP_Queryの「date_query」で発生するインデックススキャンを回避する – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

WP_Queryの`date_query`が悪夢を生む理由:インデックススキャンを死守せよ

コードレビューをしていて、もっとも冷や汗をかく瞬間の一つがこれだ。数百万件規模に膨れ上がった大規模サイトで、次のようなコードが平然とプロダクション環境に投入されているのを見たとき。

// 最悪な実装例:これだけでMySQLのCPU使用率は跳ね上がる
$query = new WP_Query([
‘post_type’ => ‘event’,
‘posts_per_page’ => 10,
‘date_query’ => [
[
‘after’ => ‘2023-01-01’,
‘before’ => ‘2023-12-31’,
‘inclusive’ => true,
],
],
]);

一見、何の問題もない美しいWordPressのクエリコードに見えるだろう。しかし、データベースの内部(MySQL / InnoDB)の挙動を知るエンジニアであれば、この数行が「全件走査(フルテーブルスキャン)の招待状」であることを瞬時に見抜くはずだ。

今回は、中級から上級へステップアップしようとしているWebエンジニアに向けて、`WP_Query`の`date_query`が背後で発行するSQLの闇を暴き、インデックスを完全に活かしてクエリを極限まで高速化する実務的アプローチを解説する。

—

1. なぜ `date_query` はフルテーブルスキャンを引き起こすのか

まずは、WordPressのコアが内部で何をしているのかを理解しよう。
`WP_Query` に `date_query` を渡すと、WordPressは `posts` テーブルの `post_date`(または `post_date_gmt`)カラムに対して条件を組み立てる。

しかし、ここで発生する最大の罠が 「暗黙の型変換(Implicit Type Conversion)」 や 「関数・演算子の使用によるインデックスの無効化」 だ。

MySQLのB-Treeインデックスは、カラムの値そのものがソートされた状態で保持されているからこそ高速に機能する。しかし、クエリ側でカラムに対して何らかの処理を加えたり、不適切なデータ型と比較させたりすると、MySQLはインデックスのツリー構造をたどることを諦め、すべての行を1つずつ舐めていくフルテーブルスキャンに切り替える。

さらに、`date_query` は複数の条件(年、月、日、時間、タイムゾーンの補正など)が絡み合うと、SQLの `WHERE` 句内で複雑な `AND` / `OR` や、場合によっては `CAST()` や `CONCAT()` などの演算を引き起こす。これが、数百万レコードを持つサイトでデータベースを窒息させる主因だ。

—

2. データベース側のインデックス設計:複合インデックスの正しい貼り方

`date_query` を最適化する第一歩は、PHPコードを書くことではなく、MySQLのスキーマ(インデックス)を正しく理解し、設計することだ。

標準のWordPressでは、`posts` テーブルには `post_date` 単体のインデックスが貼られていないケースもある(プライマリキーや他のキーとの兼ね合い、あるいは古いバージョンの名残)。また、`post_type` や `post_status` と組み合わせた検索がほとんどであるため、複合インデックス(Composite Index) の設計が必須となる。

黄金のインデックス構成

イベントやカスタム投稿を日付範囲で絞り込む場合、以下の順序で複合インデックスを構築するのが最も効率的だ。

— 推奨されるインデックス設計
ALTER TABLE wp_posts ADD INDEX idx_pt_status_date (post_type, post_status, post_date);

なぜこの順序なのか?(Leftmost Prefix原則)
1. `post_type`: 等価検索(`=`)なので、最初に配置することで一気にレコードを絞り込める。
2. `post_status`: これも等価検索(`=`)。`publish` などのステータスでさらに絞り込む。
3. `post_date`: 範囲検索(`>=`, `<=`)。インデックスの末尾にあるため、前の2つで絞り込まれた極小のインセットに対して高速な範囲スキャンが適用される。 このインデックスが存在するかどうかで、EXPLAIN結果の `type` カラムは `ALL`(フルスキャン)から `range`(インデックス範囲スキャン)へと劇的に劇変する。 ---

3. 【プロダクションコード】インデックスを死守する `WP_Query` の最適化実装

データベース側の準備ができたら、次はPHP側だ。
`date_query` は便利だが、内部生成されるSQLの制御が難しいため、極限までパフォーマンスを追求する現場では、あえて `date_query` を使わず、`posts_where` フィルターで直接安全なSQLフラグメントを注入する という手法が取られることがある。

しかし、保守性を考慮し、ここでは「`date_query` を使いつつインデックスを完全に活用する設計パターン」と、より低レベルで安全なアプローチの両方を提示しよう。

パターンA: `date_query` を安全に使う(プレフィックスを意識したクエリ)

WordPress 5.3以降、日付のハンドリングは改善されているが、パフォーマンスを担保するためには「余計な演算を発生させない文字列フォーマット」で日付を渡すことが鉄則だ。

/

  • インデックスを効率的にヒットさせる安全なWP_Queryラッパー
  • @param string $start_date ‘Y-m-d H:i:s’
  • @param string $end_date ‘Y-m-d H:i:s’
  • @return WP_Query

/
function get_optimized_date_range_posts( string $start_date, string $end_date ): WP_Query {
return new WP_Query([
‘post_type’ => ‘event’,
‘post_status’ => ‘publish’,
‘posts_per_page’ => 20,
‘orderby’ => ‘post_date’,
‘order’ => ‘ASC’,
‘no_found_rows’ => true, // ページネーションのSQL_CALC_FOUND_ROWSを殺して高速化
‘update_post_meta_cache’ => false, // メタデータを使わないなら必ず切る
‘update_post_term_cache’ => false, // タームを使わないなら必ず切る
‘date_query’ => [
[
‘column’ => ‘post_date’,
‘after’ => $start_date,
‘before’ => $end_date,
‘inclusive’ => true,
‘compare’ => ‘BETWEEN’, // 明示的にBETWEENを使用させる
],
],
]);
}

なぜこの設定が優れているのか?

1. `no_found_rows => true`: これにより、全件数を数える重い `SELECT FOUND_ROWS()` が発行されなくなる。無限スクロールや通常のリスト表示では必須の最適化。
2. `update_post_meta_cache / term_cache => false`: 不要なキャッシュクエリ(`JOIN`の嵐)を完全に排除し、必要な投稿データのみを純粋に取得する。
3. `compare => ‘BETWEEN’`: 内部的に `BETWEEN ‘…’ AND ‘…’` のSQL句に翻訳され、MySQLのB-Treeインデックスの範囲スキャン(Range Scan)に最も効率よくヒットする。

—

4. テクニカルリードからの最終チェックリスト

コードレビューでこの手のクエリを見かけたら、以下のポイントを必ず確認してほしい。

  • [ ] EXPLAINを取ったか?: `EXPLAIN SELECT …` を実行し、`type` カラムが `ref` や `range` になっているか確認したか? `ALL` になっていればインデックス設計かクエリの敗北である。
  • [ ] タイムゾーンの罠を踏んでいないか?: `post_date`(サイトのローカル時間)と `post_date_gmt`(UTC)のどちらを検索対象にしているか意識しているか? `date_query` で暗黙的なGMT変換が発生すると、インデックスが効かなくなるケースがある。原則として、検索カラムと渡す日時のタイムゾーンは一致させよ。
  • [ ] メタデータ(カスタムフィールド)での日付保持を避けているか?: 最もやってはいけないのが、日付を `wp_postmeta` に格納して `meta_query` で日付範囲検索することだ。EAVモデルのメタテーブルでこれをやると、どれだけインデックスを貼っても結合地獄によりクエリは破綻する。日付は必ず `wp_posts.post_date` または専用のカスタムテーブルに持たせよ。

まとめ

WordPressは「手軽なCMS」と呼ばれることが多い。しかし、内部構造を理解せずに雑なコードを書けば、数万〜数百万レコードの壁にぶつかった瞬間にシステムは崩壊する。

データベースのインデックス構造を脳内に描き、WP_QueryがどのようなSQLを吐き、MySQLのエンジンがどう解釈するのかをトレースする。この視点を持つことこそが、真にスケーラブルなWordPressアプリケーションを構築するエンジニアの条件だ。

妥協のないコードを書き続けよう。

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