ページネーションの深淵:SQL_CALC_FOUND_ROWSという名の技術的負債を葬る
WordPressの `WP_Query` は、開発者に対して驚くほどの抽象化を提供してくれる。しかし、その抽象化の代償として、大規模サイトにおいて致命的なパフォーマンスボトルダウンを引き起こす「時限爆弾」が隠されている。
それが `SQL_CALC_FOUND_ROWS` だ。
この記事では、MySQLの内部構造を紐解き、なぜこのオプションが大規模データセットにおいて「死」を招くのか、そしてそれを回避するためのエンジニアリング的解法を提示する。
—
1. 殺意を抱くクエリ:SQL_CALC_FOUND_ROWSの呪い
`WP_Query` を実行する際、WordPressはデフォルトで全件数をカウントしようとする。これが内部で発行されるSQLに含まれる `SQL_CALC_FOUND_ROWS` だ。
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE … LIMIT 0, 10;
SELECT FOUND_ROWS();
MySQLのクエリプランナにとって、この命令は「インデックスを駆使して高速に10件を見つける」という最適化の余地を完全に奪う。テーブル全体をスキャンし、条件に合致する全ての行を計算した上で、ようやく最初の10件が返される。レコード数が数百万単位に達した瞬間、このクエリはCPUとI/Oを飽和させ、MySQLの実行キューを埋め尽くす。
結論から言おう。大規模サイトにおいて、総件数(Found Rows)を正確に知る必要性はどれほどあるのか? ユーザー体験を損なうほどの遅延を許容してまで表示すべき数値ではないはずだ。
—
2. インデックスチューニングと「オフセット回避」の技術
ページネーションの最適化における定石は、`no_found_rows => true` を指定し、正確なカウントを放棄することだ。
$query = new WP_Query([
‘posts_per_page’ => 10,
‘paged’ => $paged,
‘no_found_rows’ => true, // これが最重要。FOUND_ROWSを無効化する
‘update_post_meta_cache’ => false, // メタデータ取得によるN+1クエリを回避
‘update_post_term_cache’ => false,
]);
しかし、単にカウントを消すだけでは不十分だ。ページが深くなるにつれ、`OFFSET` の計算コストが増大する。SQLレベルでは、`LIMIT 100000, 10` は「10万10行をフェッチし、最初の10万行を捨てる」という処理を意味する。
シニアエンジニアが採用すべき「キーセット・ページネーション」
ページ番号(`paged`)によるオフセット指定を捨て、最後に取得したIDを基準にする「キーセット(Seek Method)」へ移行せよ。
// 前ページの最後のIDを基準にする(カーソルベース)
$last_id = get_query_var(‘last_id’);
$args = [
‘posts_per_page’ => 10,
‘no_found_rows’ => true,
‘meta_query’ => [
[
‘key’ => ‘post_id’,
‘value’ => $last_id,
‘compare’ => ‘<',
'type' => ‘NUMERIC’,
]
],
‘orderby’ => ‘ID’,
‘order’ => ‘DESC’
];
この手法なら、`WHERE ID < 12345 LIMIT 10` というクエリになり、インデックス(主キー)が効く。走査範囲は常に一定となり、データセットの規模に関わらず定数時間(O(1))でレスポンスが返る。 ---
3. キャッシュ戦略とメモリの再構築
WordPressのコアにおいて、`WP_Query` の結果は `post_cache` や `meta_cache` と複雑に連動する。パフォーマンスを極限まで引き出すなら、これらの自動キャッシュを無効化し、自前で `wp_cache_get` を用いて、JSON形式等でシリアライズされたデータをRedis等のメモリ空間から直接引き抜く設計が求められる。
実装の極意:クエリの完全遮断
もし貴殿が「1ページ目」の表示速度を極限まで追求するなら、`pre_get_posts` フックでクエリ自体を書き換えるのではなく、クエリを投げないという選択肢も持て。
add_action(‘pre_get_posts’, function($query) {
if ($query->is_main_query() && !is_admin()) {
// 特定の条件下でクエリ発行を抑制し、独自APIからキャッシュを返す
if (is_home()) {
$query->set(‘suppress_filters’, true);
// 本来のクエリ実行を回避し、自前でメモリキャッシュをHITさせる
}
}
});
—
結びに代えて:アーキテクチャの尊厳
WordPressは元々、動的なブログ構築ツールとして設計された。しかし、それを大規模アーキテクチャへと適応させるには、コアの「便利機能」を一つずつ疑い、解体していく必要がある。
`SQL_CALC_FOUND_ROWS` を無効化することは、単なる設定変更ではない。それは、システムエンジニアとして、「データベース負荷」と「ユーザー体験」のトレードオフを計算し、制御下に置くという意思表示である。
システムを掌握せよ。便利さに甘んじるな。コードの裏側にあるMySQLのエンジンの鼓動を聴き、真にスケーラブルな構造をその手に実装することを期待する。