こんにちは!WordPressのコア開発や大規模Webシステムの最適化を担当している先輩エンジニアです。
他の言語(GoやRuby、Pythonなど)からWordPressの世界へやってきた方や、PHPの基礎を終えて「いよいよ本格的に大規模サイトを高速化したい!」と考えている皆さん、データベース(DB)のチューニングでこんな疑問を持ったことはありませんか?
> 「MySQLやMariaDBには『クエリキャッシュ』があるんだから、同じページが表示されるならDB側で勝手に高速化してくれるんじゃないの?」
実はここ、多くの開発者が最初にぶつかる大きな壁なんです。結論から言うと、WordPressのような動的CMSにおいて、データベース層のSQLキャッシュだけに頼るパフォーマンス改善はほぼ無力に等しいのが現実です。
今回は、`WP_Query`の内部実行フローを紐解きながら、なぜDBのSQLキャッシュが通用しないのか、そして私たちがなぜアプリケーション層(WordPress / PHP)のキャッシュ戦略をマスターしなければならないのかを、基本から本質までじっくり丁寧に解説していきますね!
ここをクリアすれば、WordPressのデータフェッチの構造はバッチリマスターできますよ。一緒に学んでいきましょう!
—
1. そもそも「MySQLのSQLクエリキャッシュ」とは?
まずはデータベース側の仕組みを振り返っておきましょう。
MySQLに昔から備わっていた「クエリキャッシュ(Query Cache)」は、クライアントから送信された`SELECT`文の「クエリ文字列そのもの」をキーにして、実行結果のレコードセットをメモリに保持する仕組みです。
[WordPress] — ( “SELECT FROM wp_posts WHERE ID = 10” ) —> [ MySQL ]
│
┌─────────────────────────────────────────────────────────────────┘
▼
【SQLクエリキャッシュの判定】
・完全に同一のSQL文字列か?(空白や大文字小文字も完全一致が必要)
・対象テーブルに変更(INSERT / UPDATE / DELETE)が入っていないか?
├── 一致 & 変更なし ──▶ [ キャッシュから即座に結果を返却(超高速) ]
└── 不一致 or 変更あり ──▶ [ ストレージエンジンで実際にクエリを実行 ]
一見すると「これで十分速くなりそう!」と思えますよね。
しかし、この仕組みにはWordPressにおいて致命的な2つの限界が存在します。
限界①:テーブルが1箇所でも更新されると「全破棄」される
MySQLのクエリキャッシュは、テーブル単位で無効化されます。つまり、`wp_posts`テーブルの「どれか1つの記事」が更新されたり、コメントが1件投稿されたり、PV数カウントのカスタムフィールドが更新された瞬間に、`wp_posts`に関連するキャッシュがすべて蒸発(Invalidate)します。
アクセスの多いサイトや動的なサイトでは、キャッシュが作られた数ミリ秒後に破棄されるため、ヒット率がほぼ0%になってしまうのです(※そのため、MySQL 8.0ではクエリキャッシュ機能自体が完全に廃止されました)。
限界②:完全一致の呪縛と動的クエリ
`SELECT FROM wp_posts WHERE post_type = ‘post’` と `SELECT FROM wp_posts WHERE post_type = ‘post’`(スペースが2つ)は、別クエリとして扱われます。ログイン状態、ユーザー権限、現在時刻、ランダムシードなどのパラメータが混ざると、二度と同じSQL文字列にならないことも珍しくありません。
—
2. WP_Queryが発行するSQLを解剖してみよう
では、私たちが普段何気なく書いている`WP_Query`が、内部でどのようなSQLを生成しているか見てみましょう。
// 最新の公開記事を5件取得する標準的なクエリ
$args = array(
‘post_type’ => ‘post’,
‘post_status’ => ‘publish’,
‘posts_per_page’ => 5,
);
$query = new WP_Query( $args );
このコードが実行されると、WordPressの内部コア(`wp-includes/class-wp-query.php`)は、以下のような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’ OR wp_posts.post_status = ‘private’)
AND wp_posts.post_date <= '2023-10-27 12:00:00' -- 現在日時が入る場合がある
ORDER BY
wp_posts.post_date DESC
LIMIT
0, 5;
-- さらにページャーのために総件数を取得する
SELECT FOUND_ROWS();
ここで注目してほしいのが、「SQLはIDの一覧(主キー)しか取得していない」という点です(これをコア内部ではSplit Queryパターンと呼びます)。
その後、WordPressは取得したID群(例: `[101, 102, 103, 104, 105]`)をもとに、記事本体データ(`post_title`, `post_content`など)やカスタムフィールド(`postmeta`)を別個に取得しに行きます。
つまり、`WP_Query`の実行フローは次のようになっています。
[ WP_Query 実行フロー ]
1. 条件から投稿IDを特定する「ヘビークエリ」(JOINやWHEREが多い)
│
▼ 投稿IDの配列 [101, 102, 103] を取得
2. 投稿本体データを取得(wp_posts)
│
▼
3. メタデータを一括取得(wp_postmeta: update_post_meta_cache)
│
▼
4. タクソノミーを一括取得(wp_term_relationships: update_post_term_cache)
データベース任せのキャッシュだと、この複雑なステップのどこか1つで更新があるだけで全体のパフォーマンスが破綻してしまいます。
だからこそ、WordPress(アプリケーション側)がメモリ上にデータを持ち、必要なパーツだけを賢く再利用する「オブジェクトキャッシュ戦略」が必須になるわけです!
—
3. アプリケーション層で戦う:Object CacheとTransient API
WordPressには、データベースに負荷をかけないための強力なキャッシュ機構が標準で組み込まれています。それがObject Cache(オブジェクトキャッシュ)です。
- WP Object Cache (`wp_cache_`): PHPの実行中(1リクエスト内)でクエリ結果をメモリに保持。RedisやMemcachedを導入すると、リクエストを跨いで永続化可能。
- Transient API (`get_transient` / `set_transient`): 有効期限付きのキャッシュ。Redis等のバックエンドがあればメモリに、なければ`wp_options`テーブルに保存される。
【リクエストの流れ】
ブラウザ ──▶ [ WordPress ] ──① キャッシュ確認──▶ [ Redis / Memcached ]
│ │ (Hit!)
│ └──▶ 爆速でデータ返却
│ (Miss…)
└──② 重いSQLを発行──▶ [ MySQL ]
│
┌──────────────────────────┘
▼
[ 取得結果をRedisに保存して返却 ]
—
4. 実践:WP_Queryをオブジェクトキャッシュで極限まで最適化する
それでは、現場でそのまま使える「プロのクエリ最適化コード」を書いてみましょう。
高負荷なランキングクエリや、トップページの複雑な集計クエリをキャッシュするベストプラクティスです。
/
- 高度な条件の投稿一覧を安全かつ高速に取得する関数
- @param int $limit 取得件数
- @return WP_Post[] 投稿オブジェクトの配列
/
function get_optimized_featured_posts( int $limit = 5 ): array {
// 1. クエリの条件に応じた一意なキャッシュキーを生成する
$cache_key = ‘my_featured_posts_’ . $limit;
$cache_group = ‘site_queries’;
// 2. まずはアプリケーション層(Redis / メモリ)にキャッシュがあるか確認
$cached_post_ids = wp_cache_get( $cache_key, $cache_group );
if ( false === $cached_post_ids ) {
// キャッシュが存在しない(Cache Miss)場合のみ、DBへクエリを発行する
$query_args = array(
‘post_type’ => ‘post’,
‘post_status’ => ‘publish’,
‘posts_per_page’ => $limit,
‘meta_key’ => ‘is_featured’,
‘meta_value’ => ‘1’,
// 【超重要】パフォーマンス向上のためのコア指定
‘fields’ => ‘ids’, // IDのみ取得(メモリ節約・Split Queryの最大活用)
‘no_found_rows’ => true, // ページネーション不要ならSQL_CALC_FOUND_ROWSを無効化
‘update_post_meta_cache’ => false, // この時点ではメタキャッシュ更新を遅延
‘update_post_term_cache’ => false, // この時点ではタームキャッシュ更新を遅延
);
$custom_query = new WP_Query( $query_args );
$cached_post_ids = $custom_query->posts; // IDの配列が返る(fields => ids のため)
// 3. 取得したID一覧を1時間(3600秒)キャッシュに保存
wp_cache_set( $cache_key, $cached_post_ids, $cache_group, 3600 );
}
if ( empty( $cached_post_ids ) ) {
return array();
}
// 4. 【神技】ID一覧から投稿オブジェクト・メタデータ・タームを一括プライミング(キャッシュ充填)する
// これにより、ループ内で get_post_meta() や the_title() を呼んでも追加のSQLは一切発行されません
_prime_post_caches( $cached_post_ids, true, true );
// 5. キャッシュからWP_Postオブジェクトの配列を復元して返却
return array_map( ‘get_post’, $cached_post_ids );
}
コードの重要ポイント解説!
1. `’fields’ => ‘ids’` の指定
`WP_Query`に巨大なオブジェクトを作らせず、主キーである`ID`(数値)の配列だけを取得させています。キャッシュするデータサイズが最小限になります。
2. `’no_found_rows’ => true`
WordPressはデフォルトで「全件で何ページあるか」を計算するために`SQL_CALC_FOUND_ROWS`を発行します。ページ送りが不要な箇所ではこれを`true`にして無駄な集計をカットしましょう。
3. `_prime_post_caches()` 関数の活用
WordPress 6.1以降でさらに強化されたコア関数です。複数の投稿IDを渡すことで、1回のクエリで全投稿のメタデータとタクソノミーを内部キャッシュ(Object Cache)にまとめてロードしてくれます。N+1問題を根本から撲滅できる魔法のような関数です!
—
5. 初学者が陥りやすい落とし穴と文法エラー
キャッシュを実装する際によくあるミスも押さえておきましょう。
落とし穴①:オブジェクトそのものをまるごとキャッシュしてしまう
// ❌ 悪い例:WP_Query オブジェクト全体をキャッシュする
$my_query = new WP_Query( $args );
set_transient( ‘my_heavy_query’, $my_query, 3600 );
`WP_Query`オブジェクトは内部に大量のプロパティやメソッド、循環参照を含んでいます。これをシリアライズ(文字列化)して保存すると、キャッシュサイズが肥大化し、逆にRedisやDBのボトルネックになります。「キャッシュするのはIDの配列だけにする」のが鉄則です。
落とし穴②:キャッシュの破棄(Invalidation)を忘れる
記事を更新したのに、古いキャッシュが残り続けて表示が変わらない問題です。記事保存フックを使ってスマートに掃除しましょう。
/
- 記事が保存・更新されたときにキャッシュを削除する
/
function flush_featured_posts_cache( int $post_id ): void {
// リビジョンの自動保存時は無視
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// 関連するキャッシュキーを削除
wp_cache_delete( ‘my_featured_posts_5’, ‘site_queries’ );
}
add_action( ‘save_post’, ‘flush_featured_posts_cache’ );
—
まとめ:キャッシュのレイヤーを意識しよう!
今回の内容を振り返ってみましょう。
1. MySQLのSQLキャッシュは、テーブル更新頻度が高いWordPressでは無力化されやすく、現代のWeb最適化の解にはならない。
2. `WP_Query`は「IDの特定」と「データのハイドレーション(結合)」を分割して処理している。
3. 高速化の鍵は、アプリケーション層(Object Cache / Transient)でIDリストを保持し、`_prime_post_caches()`等で一括取得すること。
データベースという下流のレイヤーに負荷を押し付けるのではなく、PHPという上流のレイヤーで適切にデータをコントロールできるようになると、あなたの書くコードのパフォーマンスは何倍にも跳ね上がりますよ。
ぜひ今日から、ご自身のテーマやプラグインのクエリを見直してみてくださいね。一歩ずつ、本物のフルスタックエンジニアへの階段を登っていきましょう!