【実務・中級編】実務中級者向け:WP_Queryの「fields => ‘ids’」と「get_posts」のメモリ消費量の詳細比較 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

WP_Queryの限界を超える:`fields => ‘ids’` と `get_posts` のメモリ消費量ガチ比較と最適解

コードレビューをしていると、未だに数万件規模の投稿を持つ大規模サイトで、安易に `new WP_Query()` を回しているコードに遭遇する。「動いているからいいだろう」という認識は、プロダクション環境のスケール時には致命的なメモリリークやDBの負荷増大を引き起こす。

今回は、WordPressエンジニアとして知っておくべき、オブジェクト生成コストの最適化手法について徹底的に解剖する。特に、IDのみが必要なケースにおける `WP_Query( [ ‘fields’ => ‘ids’ ] )` と、ラップ関数である `get_posts()` の挙動の違いを、メモリ消費量と内部クエリの観点からロジカルに紐解く。

中級から一歩抜け出し、シニアとして「なぜその書き方を選ぶのか」を説明できるエンジニアになるための知見を授けよう。

—

1. 内部アーキテクチャの理解:なぜ `WP_Query` は重いのか?

まず前提として、WordPressの `WP_Query` が裏で何をやっているかを正確に把握する必要がある。

デフォルトの `WP_Query` は、発行されたSQLの結果セット(通常は投稿IDのリスト)を受け取った後、以下の処理をループ内で実行する。

1. DBからのデータ取得: `SELECT FROM $wpdb->posts WHERE …` により、投稿の全カラム(コンテンツ、抜粋、スラッグ等)のデータを取得。
2. オブジェクトの構築: 取得したレコードを基に、`WP_Post` クラスのインスタンスを生成。
3. オブジェクトキャッシュへの登録: 生成された `WP_Post` オブジェクトが `wp_cache_set` を通じてオブジェクトキャッシュ(Memcached / Redisなど)に保存、あるいはキャッシュからフェッチされる。
4. メタデータの事前ロード (Meta Lazy Loading): 近年のバージョンでは最適化されているが、それでも関連するメタデータ群の効率化処理が走る。

数千件単位の投稿をこのフローで処理した場合、PHPのメモリ制限(`memory_limit`)に到達するのは時間の問題だ。特に共有ホスティングやリソースが限られたクラウド環境では、OOM(Out of Memory)エラーの引き金になる。

—

2. `fields => ‘ids’` の真価と限界

このオーバーヘッドを回避するために用意されているのが、`WP_Query` の引数における `’fields’ => ‘ids’` だ。

$query = new WP_Query( [
‘post_type’ => ‘post’,
‘posts_per_page’ => 5000,
‘fields’ => ‘ids’, // ここがキモ
] );

何が起きているのか?

内部のSQL生成フェーズにおいて、WordPressは `SELECT $wpdb->posts.ID` のみを実行するようになる。これにより、以下のメリットが生まれる。

  • ネットワーク転送量の激減: 巨大な `post_content` などのテキストフィールドがDBからPHPへと転送されない。
  • メモリ消費の劇的抑制: `WP_Post` オブジェクトではなく、単なる整数の配列(`int[]`)が返されるため、PHPのメモリフットプリントが数分の一、あるいは数十分の一になる。

注意すべき罠

ただし、`’fields’ => ‘ids’` を使った場合でも、WordPressは内部でオブジェクトキャッシュに余計なデータを残さないためのケアを意識する必要がある。もし取得したID群に対して個別に `get_post($id)` をループで回すような愚行を犯せば、結局キャッシュヒット率が低下し、N+1問題の亜種を引き起こす。IDのみで完結する処理(例:カスタムテーブルへのバッチ処理、外部APIへのID同期など)でのみ真価を発揮する。

—

3. `get_posts()` との比較:ラッパー関数の正体

次に、多くの開発者が日常的に使用している `get_posts()` についてだ。
「`WP_Query` を直接書くのは面倒だから `get_posts()` を使おう」というノリで使っていないだろうか?

結論から言えば、`get_posts()` は内部で `new WP_Query()` をインスタンス化しているだけのラッパー関数に過ぎない。

ソースコード(`wp-includes/post.php`)を覗いてみれば一目瞭然だが、`get_posts()` の実体はこうだ。

function get_posts( $args = null ) {
// デフォルト引数のマージ処理…
$my_query = new WP_Query();
$raw_posts = $my_query->query( $args );
// …
return $raw_posts;
}

決定的な違いは、`get_posts()` のデフォルト引数が `WP_Query` と異なっている点である。
例えば、`get_posts()` はデフォルトで `suppress_filters => true` が設定されており、投稿の取得パフォーマンスに重きを置いている。

しかし、もしあなたが「IDの配列だけ欲しい」という目的で `get_posts()` を使う場合、以下のように書くことになる。

$post_ids = get_posts( [
‘post_type’ => ‘post’,
‘posts_per_page’ => 5000,
‘fields’ => ‘ids’,
] );

これ自体は `WP_Query` を直接書くのとほぼ同等のパフォーマンスを発揮する。なぜなら、内部で `new WP_Query` が生成され、同じパスを通るからだ。

—

4. プロダクションコード例:安全かつ高速なバッチ処理の実装

大規模データセットを安全に処理するための、プロダクションクオリティのコードを示す。ここでは、メモリ溢れを防ぎつつ、IDベースで効率的に処理を回す設計パターンを採用している。

  • 大規模データセットに対するメモリ効率を考慮した投稿IDバッチ処理
  • @package TechLead\Performance
  • /

    declare( strict_types=1 );

    namespace TechLead\Performance;

    class BatchProcessor {

    /

    • 指定された投稿タイプのIDを取得し、コールバック関数で処理する
    • @param string $post_type 対象の投稿タイプ
    • @param int $batch_size 1回あたりの処理件数
    • @param callable $callback 各IDに対して実行する処理
    • @return void

    /
    public static function process_posts_by_id( string $post_type, int $batch_size, callable $callback ): void {
    $paged = 1;

    do {
    // メモリ消費を最小限に抑えるため ‘fields’ => ‘ids’ を指定
    $query_args = [
    ‘post_type’ => $post_type,
    ‘post_status’ => ‘publish’,
    ‘posts_per_page’ => $batch_size,
    ‘paged’ => $paged,
    ‘fields’ => ‘ids’,
    ‘orderby’ => ‘ID’,
    ‘order’ => ‘ASC’,
    // 管理画面の負荷軽減や無駄なSQLキャッシュを防ぐ
    ‘no_found_rows’ => true,
    ‘update_post_meta_cache’ => false,
    ‘update_post_term_cache’ => false,
    ];

    // get_posts は WP_Query のラッパー。明示的に意図を示すために使用。
    $post_ids = get_posts( $query_args );

    if ( empty( $post_ids ) ) {
    break;
    }

    foreach ( $post_ids as $post_id ) {
    // コールバック実行(例外処理は呼び出し側またはここで担保)
    try {
    call_user_func( $callback, (int) $post_id );
    } catch ( \Throwable $e ) {
    // ログ出力などのエラーハンドリング
    error_log( sprintf( ‘Error processing post ID %d: %s’, $post_id, $e->getMessage() ) );
    }
    }

    // メモリ解放の明示的促進(PHPのガベージコレクタを補助)
    unset( $post_ids );
    if ( function_exists( ‘gc_collect_cycles’ ) ) {
    gc_collect_cycles();
    }

    $paged++;

    } while ( true );
    }
    }

    // — 実行例 —
    // BatchProcessor::process_posts_by_id(‘post’, 500, function( int $post_id ) {
    // // ここに重い処理(外部API連携やメタデータの書き換えなど)を記述
    // });

    コードの設計ポイント

    1. `no_found_rows => true`: ページネーション用の `SQL_CALC_FOUND_ROWS` を無効化することで、MySQL側のクエリ実行コストを劇的に削減している。件数カウントが不要なバッチ処理では必須の設定。
    2. キャッシュ制御フラグの徹底: `update_post_meta_cache` と `update_post_term_cache` を `false` に。IDしか取得しないため、メタやタームのプリロードは完全な無駄骨となる。これらを防ぐことでDBへの負荷を最小化する。
    3. 明示的なメモリ管理: ループの終わりに `unset()` と `gc_collect_cycles()` を挟み、長時間稼働するバッチプロセスでもメモリリークが起きない堅牢な構造にしている。

    —

    5. まとめ:シニアエンジニアが選ぶべき最適解

    • `WP_Query` 直接 vs `get_posts()`:

    機能的な差はほぼない(`get_posts` は単なるエイリアス)。ただし、コードの可読性やデフォルト引数の特性(`suppress_filters` 等)を理解した上で使い分けること。IDのみが必要なケースではどちらを使っても内部の最適化恩恵は受けられる。

    • `fields => ‘ids’` の活用:

    「全データが必要なわけではない(IDだけ、あるいは後から必要なものだけピンポイントで取得したい)」という要件において、メモリ爆発を防ぐための最強の武器。

    • 周辺オプションの同時指定:

    `’fields’ => ‘ids’` を使うだけでなく、`no_found_rows` やメタ・タームキャッシュの無効化をセットで運用して初めて、真のパフォーマンスチューニングと言える。

    「動くコード」から「スケールするコード」へ。WordPressの内部挙動を完全に掌握し、リソースを極限まで最適化された美しいシステムを構築しよう。

    タイトルとURLをコピーしました