コードレビューの現場で、次のような`WP_Query`の記述を見かけるたびに、私はエンジニアとしての危機感を覚える。
// よくある「何も考えられていない」クエリ
$query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => 10,
]);
「たった10件の投稿を取得しているだけだから問題ない」——そう思ったならば、MySQLとWordPressの内部挙動を根本から見直す必要がある。この数行の背後で、データベースサーバーに対してどれほど暴力的な負荷がかかっているかを知らなければ、大規模サイトのテクニカルリードは務まらない。
今回は、`no_found_rows => true` という一見地味なパラメータが、なぜ数万〜数百万レコード規模のサイトにおいて生死を分ける最適化になり得るのか。その内部メカニズムと、実務で即座に採用すべき堅牢な設計パターンをロジカルに解説しよう。
—
1. なぜ `WP_Query` はデフォルトで遅いのか?
WordPressでカスタムクエリを発行した際、背後で何が起きているか意識したことはあるだろうか?
実は、`WP_Query` はデフォルトの状態だと、私たちが意図している「limitされた数件のデータ」だけでなく、「条件にヒットする全レコードの総数」を同時に計算しようとする。
ここで発行されるMySQLのクエリをログで確認したことがあるエンジニアなら、背筋が凍るはずだ。SQLには必ずと言っていいほど、以下の修飾子が含まれている。
SELECT SQL_CALC_FOUND_ROWS wp_posts.
FROM wp_posts
WHERE 1=1 AND wp_posts.post_type = ‘post’ AND wp_posts.post_status = ‘publish’
ORDER BY wp_posts.date DESC
LIMIT 0, 10;
この `SQL_CALC_FOUND_ROWS` こがすべての元凶である。
`SQL_CALC_FOUND_ROWS` の暗いコスト
MySQLは、`LIMIT`句によって「最初の10件」が見つかった時点でスキャンを終了せず、LIMIT句を無視して条件に一致する全行をスキャンし続ける。
なぜなら、後続の `SELECT FOUND_ROWS();` というクエリで「もしLIMITがなかったら何件ヒットしていたか」を返す必要があるからだ。
結果として何が起きるか?
- キャッシュが効いていない状態でのフルテーブルスキャン(あるいはインデックスの非効率な全件走査)
- クエリ実行時間の劇的な増大
- データベースのCPU使用率の跳ね上がり
- 同時アクセス数が増えた際のコネクション枯渇(MySQLのプロセスリストが詰まる)
「ページネーション(全何ページあるか)」を計算するために、毎リクエスト全件カウントのコストを支払わされているのだ。トップページや新着一覧、あるいはウィジェット用の軽量なクエリで、全件カウントなど必要だろうか? 答えは明白である。不要だ。
—
2. 救世主:`no_found_rows => true` のメカニズム
この無駄な全件カウントを完全にバイパスするのが、`WP_Query` の引数にある `no_found_rows => true` である。
$query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => 10,
‘no_found_rows’ => true, // ここがキモ
]);
これを指定すると、WordPressは内部で `FOUND_ROWS()` 用の処理をスキップし、発行されるSQLは以下のようになる。
SELECT wp_posts.
FROM wp_posts
WHERE 1=1 AND wp_posts.post_type = ‘post’ AND wp_posts.post_status = ‘publish’
ORDER BY wp_posts.date DESC
LIMIT 0, 10;
`SQL_CALC_FOUND_ROWS` が綺麗に消え去っているのがわかるだろうか。
MySQLは「10件見つかった段階」で即座に処理を打ち切るため、データベースの負荷は劇的に軽減される。特にデータ量が数万件を超えたテーブルにおいて、この差はレスポンスタイムが「数秒」から「数十ミリ秒」に縮まるほどのインパクトを持つ。
—
3. 【実践】プロダクションコードにおける設計パターン
では、実務の現場ではどのようにこの最適化を組み込むべきか。
「ページネーションが必要なメインクエリ」と「ページネーションが不要なサブクエリ」で明確に設計を分離する必要がある。
パターンA:無限スクロールやウィジェット、API用(総件数不要)
ページャー(「1, 2, 3… 次へ」)が存在しないUIや、REST APIのエンドポイントなどで複数件の投稿を返す場合は、無条件で `no_found_rows => true` を付与する。
/
- 高速化された最新記事取得関数(ウィジェット・API向け)
- @param int $limit 取得件数
- @return WP_Post[] 投稿オブジェクトの配列
/
function get_optimized_recent_posts( int $limit = 5 ): array {
$query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => $limit,
‘no_found_rows’ => true, // ページネーション不要なのでTrue
‘update_post_meta_cache’ => false, // メタデータが不要ならここも切る
‘update_post_term_cache’ => false, // ターム(タクソノミ)が不要ならここも切る
]);
return $query->posts;
}
> Tech Lead’s Note:
> リソースを極限まで削る場合、`no_found_rows` だけでなく `update_post_meta_cache` や `update_post_term_cache` の無効化もセットで検討せよ。オブジェクトキャッシュのヒット率が低い環境において、不要なメタデータのリードはそのままI/Oのボトルネックになる。
—
パターンB:ページネーションが必要な場合どうするか?
「いや、ウチのサイトはアーカイブページでページネーションが必須なんだ。だから `no_found_rows` なんて使えない」と思ったエンジニア、待ってほしい。
メインクエリ(WordPressが自動的に発行するアーカイブクエリなど)ではデフォルトで `no_found_rows` が `false` になっているため、そのままでは全件カウントが走る。
もし大規模サイトで本当にページネーションのパフォーマンスを極限まで高めたい場合、以下のアーキテクチャ設計を推奨する。
1. 「総件数(Found Rows)」の厳密なカウントを諦め、概算(Estimates)を使う
2. ページャーの「総ページ数」ではなく、「次へ」「前へ」のカーソルベース(無限スクロール)へUIを刷新する
3. トランジェントキャッシュ(Transients API)やRedisを活用して、カウント結果をキャッシュする
特に「全何件(例: 全 54,321 件)」という表示は、データベースに対して重い負荷を強いる割に、ユーザー体験上の価値は低いことが多い。「次へ」が存在するかどうかだけが分かれば、厳密な総件数など必要ないのだ。
以下は、ページネーションを持ちつつも、重い `SQL_CALC_FOUND_ROWS` を使わずに効率的にデータを捌くためのカスタムページネーションロジックの設計例だ。
/
- 堅牢なページネーション対応クエリの構築例
- @param int $paged 現在のページ番号
- @param int $per_page 1ページあたりの件数
- @return array
/
function get_paged_posts_efficiently( int $paged = 1, int $per_page = 10 ): array {
// 1. データ取得用クエリ(総件数カウントを捨てる)
$query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => $per_page,
‘paged’ => $paged,
‘no_found_rows’ => true, // カウントをスキップして爆速化
]);
// 2. 総件数が必要な場合、Transient等でキャッシュするか、
// あるいは別アプローチ(「次のページが存在するかどうか」だけをLIMIT+1件取得で判定する)でコストを相殺する。
// ここでは「LIMIT + 1件」の手法を用いて、count() の重いクエリを回避するスマートな判定を実装する。
$check_query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => 1,
‘paged’ => $paged + 1,
‘no_found_rows’ => true,
‘fields’ -> ‘ids’, // IDのみ取得してメモリ負荷を最小化
]);
$has_next_page = $check_query->have_posts();
return [
‘posts’ => $query->posts,
‘has_next_page’ => $has_next_page,
];
}
このアプローチを取ることで、重い `SQL_CALC_FOUND_ROWS` を一切発行することなく、「次のページがあるかどうか(=次へボタンを活性化すべきか)」を極めて軽量に判定できる。
—
4. まとめ:コードレビューの基準を変えよう
明日から君のチームで書かれるコード、あるいは既存のコードベースを見直してほしい。
1. ウィジェット、REST API、カスタムAJAXハンドラー、トップページのセクション取得など、ページネーションが絡まない `WP_Query` に `no_found_rows => true` が指定されているか?
2. 無駄なメタデータやタームのキャッシュロードを行っていないか?
たった1行のパラメータ追加。しかし、それはデータベースのCPUを救い、スケーラビリティの限界を引き上げる、プロフェッショナルなエンジニアにとっての「基本中の基本」である。
動くだけのコードを書くフェーズは終わりだ。内部構造をハックし、システム全体に優美な最適化をもたらすコードを、これからも書き続けてほしい。