こんにちは!WordPressの裏側の仕組みやデータベースの最適化について、もっと深く知りたいなと感じていませんか?
今回は、多くの開発者が気づかないうちにスルーしてしまいがちな、`WP_Query`のパフォーマンス最適化の超重要ポイントについてお話ししますね。テーマはずばり、「`SQL_CALC_FOUND_ROWS`の無効化と代替カウント手法」です。
「他のプログラミング言語やフレームワークからWordPressに入ったけれど、どうもデータベース周りの挙動がブラックボックスに感じてモヤモヤする…」そんな中級者・初学者のあなたに向けて、データベースの内部挙動から丁寧にかみ砕いて解説していきます。
ここをクリアすれば、WordPressのクエリパフォーマンスをグッと引き上げるプロの技が身につきますよ。一緒にマスターしていきましょう!
—
1. なぜ、あなたのWordPressは「重い」と感じるのか?
WordPressでカスタムクエリを書くとき、以下のようなコードをよく書きますよね。
$args = array(
‘post_type’ => ‘post’,
‘posts_per_page’ => 10,
);
$query = new WP_Query( $args );
「たった10件の投稿を取得しているだけだから、データベースへの負荷なんて大したことないはず…」そう思っていませんか?
実は、何も考えずにこのコードを実行すると、WordPress(厳密にはMySQL)の内部では、私たちが想像している以上の「重たい処理」が裏でコッソリ行われているんです。その元凶こそが、今回主役として取り上げる `SQL_CALC_FOUND_ROWS` という仕組みです。
—
2. 黒幕「SQL_CALC_FOUND_ROWS」の正体と弊害
内部で何が起きているのか?
WordPressの`WP_Query`は、デフォルトの状態だと、SQLを発行する際に必ずと言っていいほどこの修飾子を付与します。イメージとしては、次のようなSQLがMySQLに投げられています。
— WordPressが内部で自動生成しているSQLのイメージ
SELECT SQL_CALC_FOUND_ROWS wp_posts.
FROM wp_posts
WHERE 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は「`LIMIT 0, 10`(最初の10件)」という条件をいったん無視して、「もし`LIMIT`句がなかったら、この条件にヒットする全データは何件あるのか?」をデータベース全体(またはインデックス)を走査して数え上げます。
- やりたいこと: 「最新の10件を取りたい!」(`posts_per_page => 10`)
- MySQLの実際の動き: 「10件取ってきたけど、条件に合うデータ自体は全部で50,000件あったよ!」と、わざわざ全件数を計算して保持する。
なぜこんなことをしているかと言うと、ページネーション(「全50ページ中、今は1ページ目です」といった表示)のために、総ページ数をあらかじめ計算しておく必要があるからですね。
データが増えるほど牙をむくパフォーマンス劣化
もし、あなたのサイトの投稿数が数万件、数十万件と増えていったらどうなるでしょう?
ユーザーが「最新の10件」を見るだけのリクエストに対しても、MySQLは毎回「全件数カウント」という重たい計算を強制されます。これが、大規模サイトにおけるWordPressのパフォーマンス低下の大きな原因の一つなんです。
「いや、トップページだからページネーションなんていらないよ」「ただ最新のバナー情報を5件表示したいだけなのに…」というケースでも、デフォルトでこの全件カウントが走ってしまうのは、エンジニアとしてはもどかしいですよね。
—
3. 解決策:`no_found_rows` で `SQL_CALC_FOUND_ROWS` を無効化する
ありがたいことに、WordPressのコア開発陣もこの問題は百も承知です。`WP_Query`の引数には、この余計なカウント処理をスパッと止めるためのスイッチが用意されています。
それが `no_found_rows => true` です!
基本的な使い方
ページネーション(全件数)が不要なクエリを書くときは、次のように引数を指定します。
‘post’,
‘posts_per_page’ => 5,
‘no_found_rows’ => true, // ← ここが今回のキモ!全件カウントを無効化
);
$custom_query = new WP_Query( $args );
if ( $custom_query->have_posts() ) :
while ( $custom_query->have_posts() ) :
$custom_query->the_post();
// 投稿の表示処理
the_title( ‘
‘, ‘
‘ );
endwhile;
wp_reset_postdata();
endif;
たったこれだけで、WordPressは内部で発行するSQLから`SQL_CALC_FOUND_ROWS`を外し、さらに「全件数を取得するための2回目のクエリ(`SELECT FOUND_ROWS()`)」の実行もスキップしてくれます。
データベースへの無駄な負荷がスッと消え、レスポンス速度が劇的に向上しますよ。ウィジェットの読み込みや、トップページのサブクエリなど、「ページャーを使わない一覧取得」には必ずこの設定を入れる癖をつけましょう。
—
4. 「でも、やっぱり総数も知りたい!」という時の代替案
「`no_found_rows => true` で速くなるのは分かったけど、無限スクロールやレイアウトの都合で、やっぱり全体の件数も把握しておきたいんだよね」という現場の要件もありますよね。
そんなときは、どうすればいいでしょうか?
実は、`SQL_CALC_FOUND_ROWS` を使ったデフォルトのカウント方法は非常に非効率なため、パフォーマンスチューニングの定石としては「カウントは別の軽量なクエリで代用する」か、「キャッシュを活用する」アプローチを取ります。
代替案:カウント専用の軽量クエリを分ける
もしどうしても総数が必要な場合は、`SQL_CALC_FOUND_ROWS`の呪縛を断ち切った上で、件数取得用のコストの低いクエリを別に実行するか、Transient API等でキャッシュします。
例えば、以下のように「投稿IDの配列だけ」を取得してカウントをPHP側で処理したり、別アプローチを取ることも可能です(※ただし、件数が数百万件規模でなければ、Transientでのキャッシュと組み合わせるのが実務では一般的です)。
‘post’,
‘posts_per_page’ => 10,
‘no_found_rows’ => true,
);
$query = new WP_Query( $args );
// 2. 総数が必要な場合は、Transient APIなどでキャッシュしつつ別途取得を検討する
// (毎回リアルタイムで重いカウントを走らせない設計にするのがプロの技です)
$total_posts = get_transient( ‘my_heavy_posts_count’ );
if ( false === $total_posts ) {
// wp_count_posts() など軽量な関数でステータスごとの数を取得する手もあります
$counts = wp_count_posts();
$total_posts = $counts->publish;
set_transient( ‘my_heavy_posts_count’, $total_posts, HOUR_IN_SECONDS );
}
このように、WordPressの内部構造(「何をするとデータベースのどこに負荷がかかるのか」)を意識できるようになると、コードを書くときの視点がガラリと変わってきます。
—
まとめ
今回は、`WP_Query`における `SQL_CALC_FOUND_ROWS` の無効化とパフォーマンス最適化について解説しました。
- デフォルトの挙動: `WP_Query`はページネーションのために自動で全件数を計算(`SQL_CALC_FOUND_ROWS`)しており、これがパフォーマンス低下の原因になる。
- 解決策: ページャーが不要なクエリには `no_found_rows => true` を指定して、無駄なカウント処理をバッサリと無効化する。
- 実務での心構え: サイトの規模が大きくなればなるほど、こうした「隠れたSQLのコスト」に気を配ることが一流のエンジニアへの第一歩。
「ここをクリアすれば、WordPressの基本はバッチリマスターできますよ!」
データベースの裏側まで見通せるフルスタックエンジニアを目指して、ぜひ今日のコードをご自身の開発環境や案件に取り入れてみてくださいね。それでは、また次の知見でお会いしましょう!