コードレビューを始めよう。
君が先ほど提出した機能の実装、フロントエンドへのデータ受け渡し部分だが……正直に言って、プロダクション環境に投入するには致命的なパフォーマンスの爆弾を抱えている。
// 【NGコード】やってはいけない典型的な全オブジェクトロード
$query = new WP_Query([
‘post_type’ => ‘product’,
‘posts_per_page’ => 50,
‘meta_key’ => ‘_stock_status’,
‘meta_value’ => ‘instock’,
]);
foreach ($query->posts as $post) {
// IDやカスタムフィールドの一部しか使っていないのに、ここで全データをメモリに展開している
echo ‘
‘;
}
このコードの何が問題か分かるか?
`new WP_Query` をデフォルトのまま実行すると、WordPressはデータベースから該当する投稿の全カラム(`post_content`, `post_excerpt`, `post_password` などの巨大なテキストフィールドを含む)をごっそり取得し、さらにそれを `WP_Post` というPHPの肥大化したオブジェクトにラップしてメモリ上に保持する。
たった50件程度なら微小な差かもしれない。だが、これが数千件規模の検索結果や、高トラフィックなAPIのエンドポイント、あるいはジョブキューのバッチ処理だったらどうなる? PHPのメモリ制限(`memory_limit`)をいとも簡単に食いつぶし、OOM(Out of Memory)エラーでサーバーを沈黙させること請け合いだ。
今回は、この無駄なメモリ消費を根絶し、データベースとPHPのI/Oを極限まで最適化するための決定版アプローチ――`fields => ‘ids’` を用いた省メモリ設計について、コアの内部挙動を踏まえながら徹底的に解説する。
—
なぜ `fields => ‘ids’` なのか? 内部コアの挙動から紐解く
まず、WordPressのデータベースレイヤーと `WP_Query` が裏で何をやっているのかを理解してほしい。
`WP_Query` のコンストラクタに `fields => ‘ids’` を指定すると、SQLの生成クエリが劇的に変化する。通常は `SELECT FROM wp_posts …` となるところが、以下のように最適化される。
SELECT wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type = ‘product’ …
1. ネットワーク帯域とメモリの圧倒的な節約
取得するのは `ID` カラムの整数値のみだ。マルチバイトの長文テキストや、シリアライズされたメタデータ、リビジョン情報は一切フェッチされない。PHP側で生成されるのも、重厚長大な `WP_Post` オブジェクトの配列ではなく、ただの整数の配列(`int[]`)である。
2. オブジェクトキャッシュ(Memcached / Redis)の効率化
WordPressのオブジェクトキャッシュ(`wp_cache_set` / `wp_cache_get`)において、巨大な `WP_Post` オブジェクトをキャッシュし続けるのはメモリの無駄遣いだ。IDの配列であれば、キャッシュサイズは最小限で済み、ヒット率の向上にも直結する。
—
プロダクションコード:堅牢なIDベースクエリの設計パターン
では、実務の現場でどのように実装すべきか。IDだけを取得した後に、必要なデータ(投稿オブジェクトやメタデータ)をどう安全かつ効率的に取得・処理するのか。
以下のコードを見てほしい。これが、シニアエンジニアがレビューで合格を出すプロダクションコードのテンプレートだ。
/
- 最適化されたプロダクションコード例:IDのみを取得し、必要なデータだけを遅延ロードする
- @return array 処理結果の配列
/
function get_optimized_in_stock_product_titles(): array {
// 1. まずは軽量なクエリでIDの配列のみを取得する
$query_args = [
‘post_type’ => ‘product’,
‘posts_per_page’ => 50,
‘fields’ => ‘ids’, // ★ここがキモ:IDのみをフェッチ
‘meta_query’ => [
[
‘key’ => ‘_stock_status’,
‘value’ => ‘instock’,
],
],
‘no_found_rows’ => true, // ページネーション用のSQL_CALC_FOUND_ROWSを抑制してさらに高速化
];
$product_ids = get_posts( $query_args ); // get_postsは内部でWP_Queryをラップし、fields => ‘ids’ なら整数の配列を返す
// 2. 該当データがなければ早期リターン(無駄な処理を走らせない)
if ( empty( $product_ids ) ) {
return [];
}
// 3. 必要なデータだけをキャッシュを効かせながら一括取得(Prime the Cache)
// WordPressコアの関数を使うことで、内部的にオブジェクトキャッシュが効く
// update_post_caches( $product_ids, array( ‘post’ ), array( ‘meta’ ) ); // 必要に応じてメタデータも一括キャッシュ
$results = [];
foreach ( $product_ids as $post_id ) {
// ここで初めて必要な情報を安全に取得する
// すでにWP_Postが必要な場合は get_post() を使う。これも内部キャッシュから即座に引かれる。
$post = get_post( $post_id );
if ( ! $post ) {
continue;
}
$results[] = [
‘id’ => $post->ID,
‘title’ => get_the_title( $post ),
‘url’ => get_permalink( $post ),
];
}
return $results;
}
—
コードレビューにおける重要なチェックポイント
上記のコードには、単に `fields => ‘ids’` を指定する以外にも、システム全体を安定させるための重要なプラクティスが散りばめられている。
1. `’no_found_rows’ => true` の併用
ページネーション(「全何件中何件目」の表示)が不要な場合、`WP_Query` はデフォルトでSQLに `SQL_CALC_FOUND_ROWS` を付与する。これが大規模テーブルにおいてテーブルスキャンを誘発し、クエリパフォーマンスを著しく低下させる最大の原因の一つだ。
IDのみを取得するようなバッチ処理やAPIレスポンスの構築では、必ず `’no_found_rows’ => true` をセットで指定すること。
2. データの取得順序(Order)の担保
`fields => ‘ids’` を使った場合でも、カスタムソート(`orderby => ‘meta_value_num’` など)は正常に機能する。データベース側で適切なソートとLIMITが適用された上で、IDの配列が返されるため、意図した順序が崩れる心配はない。
3. キャッシュの恩恵を最大化する
「IDだけ取得して、結局あとから `get_post()` するなら意味がないのでは?」と思った鋭いエンジニアへ。
そこがポイントだ。一括で軽量なクエリでIDを引っ張った後、個別にデータを引く際、WordPressのオブジェクトキャッシュ機構(またはプラグインによるRedis/Memcached連携)によって、2回目以降のアクセスや同一リクエスト内の処理でデータベースへの追加クエリが完全にバイパスされる。
—
結び:インフラとコードは常に連動している
データベースのインデックスチューニングやクエリ最適化は、単なる「SQLの書き方のテクニック」ではない。アプリケーション全体のメモリフットプリントを最小化し、サーバーリソースの限界値を引き上げるための必須のエンジニアリングだ。
次に `new WP_Query` や `get_posts` を書くときは、手を止めて自問してほしい。
「俺はいま、本当にそのすべてのオブジェクトをメモリにロードする必要があるか?」
答えがノーなら、迷わず `fields => ‘ids’` を選べ。
その小さな判断の積み重ねが、数百万アクセスに耐えうる堅牢なWordPressシステムを作り上げる。さて、君のコードの修正版プルリクエストを楽しみにしているよ。