【実務・中級編】初心者向け:get_postsとWP_Query、どちらを使うべき?クエリ発行の観点から比較 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

WP_Queryとget_postsの内部実装の真実:データベース負荷から解き明かす「正しいクエリ設計」

コードレビューの場で、こんなコードを見かけたことはないだろうか。

// レビュー対象コード
$posts = get_posts([
‘posts_per_page’ => 10,
‘post_type’ => ‘product’,
‘meta_query’ => [
[
‘key’ => ‘_stock_status’,
‘value’ => ‘instock’,
]
]
]);

一見して「動くコード」だが、シニアエンジニアの視点からは冷や汗が出る設計だ。このコードは、大規模なECサイトにおいてデータベースのCPU使用率を跳ね上げ、最悪の場合はMySQLのコネクション枯渇を引き起こす潜在的爆弾を抱えている。

「`get_posts`は手軽だから」「`WP_Query`は書く量が多いから」——そんな安易な理由でクエリ関数を選定していないだろうか。

今回は、WordPressのコア内部(`WP`クラス、`WP_Query`クラス)がデータベースに対してどのようなSQLを発行しているのかを解剖し、実務で絶対に避けるべきアンチパターンと、堅牢なプロダクションコードのための選択基準をロジカルに伝授する。

—

1. 内部実装の解剖:`get_posts()` は `WP_Query` の「ラッパー」に過ぎない

まず、大前提を共有しておこう。`get_posts()` は、独自のデータベースクエリ生成ロジックを持っているわけではない。

内部のソースコード(`wp-includes/post.php`)を覗けば一目瞭然だが、`get_posts()` は内部で `new WP_Query()` をインスタンス化し、その結果の `$query->posts` を返しているだけの「シンタックスシュガー(糖衣構文)」である。

// wp-includes/post.php の内部実装(概念コード)
function get_posts( $args = null ) {
$defaults = [
‘posts_per_page’ => 5,
‘offset’ => 0,
‘category’ => 0,
// … 略
];

// 引数をパースして WP_Query に渡す
$r = wp_parse_args( $args, $defaults );

// ★ 内部で必ずインスタンスが生成される
$the_query = new WP_Query();
$posts = $the_query->query( $r );

return $posts;
}

「なんだ、じゃあどっちを使っても同じじゃないか」と思ったなら、それはデータベースのライフサイクルとグローバルスコープの汚染という、WordPress特有の闇を見落としている。

—

2. データベース負荷とメモリ管理:決定的な2つの違い

では、単なるラッパーであるはずの2者において、なぜ実務での選択が重要になるのか。その理由は「グローバル状態の破壊」と「デフォルト引数のコスト」にある。

① グローバル `$wp_query` の汚染リスク

`new WP_Query()` を直接実行する場合、何も意識しなければグローバル変数 `$wp_query` を上書き、あるいは書き換えることになる。これはWordPressのメインクエリ(ページのパーマリンクやアーカイブ判定)を狂わせる原因となり、テンプレートの条件分岐(`is_home()`, `is_archive()` など)で予期せぬバグを引き起こす。

一方、`get_posts()` は、初期状態で `suppress_filters => true` が強制されるなど、テンプレートループやグローバル状態への干渉を最小限に抑えるように設計されている。

② デフォルト引数が引き起こす隠れたコスト

`WP_Query` は、インスタンス化された瞬間に膨大なプロパティを初期化する。
さらに見落としがちなのが、`get_posts()` に渡されるデフォルト引数だ。`get_posts()` はデフォルトで `suppress_filters => true`(SQLのフィルターフックを無効化)や `no_found_rows => false`(SQL_CALC_FOUND_ROWSによる全件カウント)の設定が、`WP_Query` のデフォルトとは異なる挙動を示す場合がある。

特に実務で致命的なのは、`no_found_rows`(ページネーション用の総件数計算を省略するフラグ)の扱いだ。

  • `WP_Query`: デフォルトは `no_found_rows = false` (`SQL_CALC_FOUND_ROWS` が実行される)
  • `get_posts`: デフォルトで内部的に `no_found_rows = true` に設定されるケースが多い(※バージョンや引数によるが、基本は軽量化指向)

`SQL_CALC_FOUND_ROWS` は、MySQLのオプティマイザにとって最悪の機能の一つだ。LIMIT句を無視してマッチする全行スキャンを強制するため、レコードが数十万件を超えた瞬間、データベースのCPU使用率が100張り付きを起こす。

—

3. 【実務判定基準】どちらを使うべきか?

エンジニアリングチームとしての明確なコーディング規約をここに定義する。

| 評価軸 | `get_posts()` を使うべきケース | `new WP_Query()` を使うべきケース |
| :— | :— | :— |
| 主な用途 | サイドバーの「最近の投稿」、特定のカスタム投稿の簡易取得など、「データを取得して配列としてループ処理したいだけ」のとき。 | メインループの拡張、複雑なページネーション(`paged`パラメータが必要)、複数のクエリを並行処理する高度なコンポーネント設計。 |
| グローバル影響 | 意識しなくてよい(安全に配列を返す)。 | `$wp_query` との衝突に細心の注意が必要(必要に応じて `wp_reset_postdata()` や `wp_reset_query()` が必須)。 |
| 拡張性 | 限定的。 | 高い。フックを挟んだり、カスタムSQLフラグメント(`posts_clauses`など)を結合しやすい。 |

—

4. プロダクションコード例:堅牢かつハイパフォーマンスなクエリ実装

では、実務の現場でスケーラビリティを担保した「美しいコード」とはどのようなものか。
ここでは、メタデータ(カスタムフィールド)とタクソノミーを複合的に条件づけし、かつデータベースに負荷をかけない(`no_found_rows`を明示的に制御した)`WP_Query`の堅牢な実装パターンを提示する。

/

  • 高負荷環境に耐えるセキュアで高速なプロダクトデータ取得関数
  • @param int $category_id 取得対象のカテゴリID
  • @param int $limit 取得件数
  • @return WP_Post[] 投稿オブジェクトの配列

/
function my_project_get_premium_products( int $category_id, int $limit = 5 ): array {
// 1. キャッシュキーの生成(Transient APIとの連携を見据えた設計)
$cache_key = ‘prem_prod_’ . $category_id . ‘_’ . $limit;
$cached_posts = get_transient( $cache_key );

if ( false !== $cached_posts ) {
return $cached_posts;
}

// 2. WP_Queryの引数定義(無駄なオーバーヘッドを徹底排除)
$args = [
‘post_type’ => ‘product’,
‘posts_per_page’ => $limit,
‘post_status’ => ‘publish’,

// ★最重要パフォーマンスチューニング:全件カウントを完全に殺す
‘no_found_rows’ => true,

// メタデータ・タームキャッシュの積極的ロード(N+1問題の予防)
‘update_post_meta_cache’ => true,
‘update_post_term_cache’ => true,

// タクソノミー絞り込み
‘tax_query’ => [
[
‘taxonomy’ => ‘product_cat’,
‘field’ => ‘term_id’,
‘terms’ => $category_id,
‘include_children’ => false, // 子タームの再帰的スキャンを抑制
],
],

// メタフィールドによる絞り込み(インデックス設計がされている前提)
‘meta_query’ => [
‘relation’ => ‘AND’,
[
‘key’ => ‘_stock_status’,
‘value’ => ‘instock’,
‘compare’ => ‘=’,
‘type’ => ‘CHAR’,
],
],
];

// 3. クエリの実行
$query = new WP_Query( $args );
$posts = $query->posts;

// 4. グローバル状態の安全な復元(WP_Query使用時の鉄則)
// wp_reset_postdata() はザ・ループ(the_post())を使う場合に必須だが、
// 配列だけを返す場合はグローバル $post の書き戻しを行うか、注意深く扱う。
// ※ WP_Query を直接使う場合は post_data の巻き戻しに注意。
wp_reset_postdata();

// 5. Transientに結果をキャッシュ(TTL: 1時間)
set_transient( $cache_key, $posts, HOUR_IN_SECONDS );

return $posts;
}

このコードが「プロダクションクオリティ」である理由

1. `no_found_rows => true` によるクエリの軽量化
MySQLが発行するSQLから `SQL_CALC_FOUND_ROWS` が排除され、インデックスヒット率が劇的に向上する。ページネーションが不要なウィジェットやセクションであれば、必ずこれを真に設定すべきだ。
2. キャッシュ層(Transient API)との融合
どれほどクエリをチューニングしても、データベースにアクセスする回数自体を減らす(キャッシュする)ことに勝る最適化はない。
3. N+1問題の事前予防
`update_post_meta_cache` と `update_post_term_cache` を `true` に明示することで、ループ内でサムネイルや価格、カテゴリ名を出力する際に発生する「隠れクエリ(N+1問題)」をバッチ処理で一網打尽にしている。

—

5. まとめ:エンジニアとしての選択眼を持て

「どっちを使うべきか?」という問いに対するファイナルアンサーはこうだ。

  • ウィジェットやデータ配列の取得、シンプルなロジックなら:

グローバル環境を汚染せず、コード量を減らせる `get_posts()` を使え(ただしパラメータのデフォルト挙動に注意すること)。

  • メインループの改変、ページネーション制御、あるいは高度なメタ・タクソノミーの複合条件を扱うなら:

コスト管理とキャッシュ戦略をコントロールできる `WP_Query` をインスタンス化し、`no_found_rows` やキャッシュを適切にチューニングして実装せよ。

WordPressは「誰でも簡単に作れるCMS」であると同時に、内部構造を理解せず実装すれば、簡単にスケールしない「レガシーシステム」に成り下がる諸刃の剣だ。
データベースの挙動から逆算し、今この関数がMySQLにどのようなSQLを投げ、どれだけの負荷を与えているのか——そのイメージを脳内で完全トレースできるようになって初めて、真のWordPressエンジニアと名乗ることができる。

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