みなさん、こんにちは!WordPressのコア開発や大規模データベースの最適化に携わっている先輩エンジニアです。
WordPressでWebサイトを運用していて、「記事数が数万件を超えたあたりから、特定期間の記事一覧ページやアーカイブページの表示が急に重くなった…」という経験はありませんか?
実はその原因の多くは、WordPressの標準クエリ発行クラスである `WP_Query` で `date_query` を使ったときに発生する「データベースの範囲検索(Range Scan)のボトルネック」にあります。
今回は、プログラミング初学者や他の言語からWordPressに入ってきた開発者のみなさんに向けて、「なぜ date_query が遅くなるのか」というMySQL内部の仕組みから、それを劇的に高速化する「複合インデックス(Composite Index)の設計」まで、分かりやすく丁寧に解説していきますね。
ここをクリアすれば、WordPressのデータベース最適化の基本はバッチリマスターできますよ!
—
1. なぜ `date_query` を使うとサイトが重くなるのか?
まずは、私たちがよく書くPHPのコードと、その裏でWordPressがMySQLに対して発行している「生のSQL文」を見比べてみましょう。
WP_Queryでの一般的な指定例
例えば、「2023年1月1日から2023年12月31日までの公開済み投稿を取得する」という処理を書いてみます。
‘post’,
‘post_status’ => ‘publish’,
‘posts_per_page’ => 10,
‘date_query’ => array(
array(
‘after’ => ‘2023-01-01’,
‘before’ => ‘2023-12-31’,
‘inclusive’ => true, // 指定日を含む (>= および <=)
),
),
'orderby' => ‘post_date’,
‘order’ => ‘DESC’,
);
$query = new WP_Query( $args );
一見、とても綺麗で問題のないWordPressコードですよね。しかし、WordPressがこのコードを解釈してMySQLに送るSQL文(RAW SQL)は、次のようになっています。
内部で発行されているRAW SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = ‘post’
AND (wp_posts.post_status = ‘publish’)
— ↓ ここが date_query によって生成された範囲検索条件!
AND ( wp_posts.post_date >= ‘2023-01-01 00:00:00’
AND wp_posts.post_date <= '2023-12-31 23:59:59' )
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
ここで注目してほしいのが、`post_date >= … AND post_date <= ...` という範囲検索(Range Scan)の部分です。
辞書で例える「範囲検索」のボトルネック
データベースの「インデックス」は、本の末尾にある「五十音順の索引」や「辞書」のようなものです。
- 完全一致 (`post_type = ‘post’`):「『さ』の項目を開く」ように、ピンポイントで場所を特定できます。
- 範囲検索 (`post_date >= ‘2023-01-01’`):「2023年1月1日〜12月31日までのページを全部めくって探す」という作業になります。
WordPressの標準テーブル `wp_posts` には、最初から `type_status_date` というインデックス(`post_type`, `post_status`, `post_date`, `ID` の組み合わせ)が用意されています。
しかし、データ量が数十万件規模になると、`post_date` に対する「範囲指定」と「ソート(ORDER BY)」が複雑に絡み合い、MySQLが「どの順番でインデックスを辿れば一番早いか」を迷ってしまうのです。その結果、最悪の場合はテーブル全体を上から順に探す「フルスキャン」や、メモリ上で並び替えを行う「Filesort」が発生し、ページ表示に数秒〜数ミリ秒の遅延が生じてしまいます。
—
2. データベースの悲鳴を聞く:「EXPLAIN」で実行計画を解剖しよう
ボトルネックの正体を突き止めるために、MySQLの `EXPLAIN`(実行計画)という命令を使って、データベース内部の動きを視覚的に覗いてみましょう。
phpMyAdminやデータベース管理ツールで、先ほど発行されたSQLの頭に `EXPLAIN` をつけて実行してみます。
EXPLAIN SELECT wp_posts.ID
FROM wp_posts
WHERE wp_posts.post_type = ‘post’
AND wp_posts.post_status = ‘publish’
AND wp_posts.post_date >= ‘2023-01-01 00:00:00’
AND wp_posts.post_date <= '2023-12-31 23:59:59'
ORDER BY wp_posts.post_date DESC;
改善が必要な危険なサイン(EXPLAINの例)
| select_type | table | type | possible_keys | key | key_len | rows | Extra |
| :— | :— | :— | :— | :— | :— | :— | :— |
| SIMPLE | wp_posts | range | type_status_date | type_status_date | 164 | 45000 | Using where; Using filesort |
ここでチェックすべきポイントは、一番右の `Extra` カラムです!
1. `Using filesort`
データベースがインデックスの順番通りにデータを読み込めず、メモリやディスク上でわざわざデータを並び替えている(重い処理)ことを意味します。
2. `rows` が大きい(例: 45000件)
10件しか取得しないのに、内部では45,000件ものデータを検証しています。
この「`Using filesort`」を消し去り、検索速度を数ミリ秒レベルに爆速化させる技術こそが、これから解説する「最適化された複合インデックス」です。
—
3. 解決策:複合インデックス(Composite Index)の設計思考
データベースに新しいインデックス(索引)を作ってあげましょう。ですが、ただ闇雲にインデックスを作れば良いわけではありません。MySQLのB-Treeインデックスには「最左前置ルール(Leftmost Prefix Rule)」という絶対的なルールがあります。
インデックスの列挙順序の黄金律
複合インデックスを作成するときは、カラムを並べる順番が命です。基本ルールは以下の通りです。
【黄金順序】
1. 完全一致で絞り込むカラム(例: post_type, post_status)
2. 範囲検索・ソートに使うカラム(例: post_date)
データベースは、左側のカラムから順番にデータを絞り込んでいきます。
- 良い順番: `(post_type, post_status, post_date)`
- まず `post_type = ‘post’` で超大幅に絞り込む。
- 次に `post_status = ‘publish’` でさらに絞り込む。
- 綺麗に整理された極小のデータ群に対して、最後に `post_date` の範囲を切り取る。
もし順序を間違えて `(post_date, post_type, post_status)` のようにしてしまうと、最初に `post_date` の広大な範囲検索が走ってしまい、それ以降の `post_type` のインデックス効果が失われてしまいます(これを「範囲条件によるインデックスの断絶」と呼びます)。
—
4. 実践:最適化インデックスの作成と自動化
それでは、実際にカスタムインデックスを作成してみましょう!
1. 手動でSQLを実行してインデックスを作成する場合
データベース管理ツール(phpMyAdminなど)や WP-CLI で以下のSQLを実行します。
— wp_posts テーブルに post_type, post_status, post_date の専用複合インデックスを作成
ALTER TABLE wp_posts
ADD INDEX idx_type_status_date_custom (post_type, post_status, post_date);
2. WordPressプラグインから安全に自動生成するコード例
実務では、サイトの有効化時やプラグインの有効化フックで、WordPressコア関数 `dbDelta()` を使って安全にインデックスを追加するのがプロの現場の標準技法です。
以下のコードを、テーマの `functions.php` や自作プラグインに記述してみましょう。
/
function my_custom_add_date_query_index() {
global $wpdb;
$table_name = $wpdb->posts;
$index_name = ‘idx_type_status_date_custom’;
// すでにインデックスが存在するかチェック(二重作成の防止)
$index_exists = $wpdb->get_var(
$wpdb->prepare(
“SHOW INDEX FROM {$table_name} WHERE Key_name = %s”,
$index_name
)
);
// インデックスが存在しない場合のみ作成処理を実行
if ( ! $index_exists ) {
// SQL文の作成(カラムの順番に注意!)
// post_type(20)のようにプレフィックス長を指定してメモリを節約
$sql = “ALTER TABLE {$table_name}
ADD INDEX {$index_name} (post_type(20), post_status(20), post_date)”;
// クエリの実行
require_once( ABSPATH . ‘wp-admin/includes/upgrade.php’ );
$wpdb->query( $sql );
// ログ出力(開発時の確認用)
error_log( ‘パフォーマンス最適化: 複合インデックス ‘ . $index_name . ‘ を追加しました。’ );
}
}
// プラグイン有効化時、またはテーマセットアップ時に一度だけ実行させる
add_action( ‘after_switch_theme’, ‘my_custom_add_date_query_index’ );
—
5. 初学者が陥りやすい3つの罠と文法・設計エラー
チューニングを行う際、初心者がハマりがちなポイントを3つまとめました。あらかじめ知っておくと、トラブルを未然に防げますよ!
罠①:インデックスを貼れば貼るほど良くなるという勘違い
「じゃあ全部のカラムにインデックスを貼ればいいのでは?」と思ってしまいがちですが、それはNGです!
インデックスを作成すると、記事の保存(INSERT)や更新(UPDATE)のたびに、データベースが裏でインデックスも書き換える必要が出てきます。インデックスが多すぎると、記事の保存が非常に重くなってしまいます。
罠②:`ORDER BY rand()` を併用してしまう
`date_query` で絞り込んだ後に `’orderby’ => ‘rand’`(ランダム表示)を指定すると、どれだけ綺麗なインデックスを作ってもデータベースは全件をメモリに展開してシャッフルするため、すべてのインデックス効果が打ち消されて無効化されます。ランダム表示が必要な場合は、別のロジックを検討しましょう。
罠③:VARCHAR型のプレフィックス長指定忘れ
文字列型のカラム(`post_type` や `post_status`)にインデックスを貼るとき、指定なしだと文字数全体をインデックス化しようとして無駄なメモリエリアを消費します。
`post_type(20)` のように文字数を制限(プレフィックス長指定)することで、インデックスサイズを小さく保ち、キャッシュに乗りやすくするのが熟練エンジニアのテクニックです。
—
6. まとめ
最後にもう一度、今回の重要なポイントをおさらいしましょう!
1. `date_query` は内部で `post_date` の「範囲検索」を生成するため、データ量が増えると遅くなりやすい。
2. `EXPLAIN` コマンドを使って `Using filesort` や対象行数(rows)を確認する習慣をつける。
3. 複合インデックスは「完全一致のカラム(post_type, post_status)」→「範囲・ソートのカラム(post_date)」の順番で並べる。
4. `dbDelta` や `$wpdb->query` を使い、テーマやプラグインから安全にインデックスを運用する。
WordPressの内部クエリとデータベース(MySQL)の仕組みを理解すると、重かったサイトが嘘のように一瞬で表示されるようになります。
「ただPHPを書く」領域から一歩抜け出して、データベースの内部構造まで掌握したワンランク上のWEBエンジニアを目指していきましょう。ここをマスターできれば、どんな大規模WordPress案件が来ても安心ですよ!
応援していますね!