コードレビューを始めよう。
君が先ほど提出したプルリクエストの`WP_Query`の記述、一見すると何の問題もないように見える。だが、MySQLの実行計画(EXPLAIN)を見たことがあるか?
実務の現場において、`WP_Query`は単なるラッパー関数ではない。これは「SQLを生成するブラックボックス」だ。内部で何が発行され、データベースのインデックスがどうヒットしているかを理解せずに書かれたコードは、データ量が数万件を超えた瞬間にサイトをスローダウンさせる時限爆弾と化す。
今回は、特に見落とされがちだがデータベースのパフォーマンスに致命的な影響を与える`post_status`(投稿ステータス)の指定方法とインデックスの関係について、内部コアの挙動からロジカルに解説しよう。
—
1. なぜ `post_status` の指定でSQLが変わるのか?
まずはWordPressのコアが発行するSQLの裏側を覗く。
`WP_Query`に渡す引数の違いによって、MySQLのオプティマイザの動きは劇的に変わる。
パターンA:デフォルト(公開済み投稿のみを対象とする場合)
$query = new WP_Query([
‘post_type’ => ‘post’,
// ‘post_status’ を指定しない場合、デフォルトは ‘publish’
]);
この時、WordPress内部(`WP_Query::get_posts()`)が生成するSQLのWHERE句は、おおむね以下のようになる。
WHERE 1=1
AND wp_posts.post_status = ‘publish’
AND wp_posts.post_type = ‘post’
パターンB:複数ステータスや「すべて」を指定する場合
$query = new WP_Query([
‘post_type’ => ‘post’,
‘post_status’ => [‘publish’, ‘draft’, ‘pending’], // または ‘any’
]);
この場合のWHERE句はどうなるか?
WHERE 1=1
AND wp_posts.post_type = ‘post’
AND ( wp_posts.post_status = ‘publish’ OR wp_posts.post_status = ‘draft’ OR wp_posts.post_status = ‘pending’ )
—
2. インデックスの観点から見た「なぜそれが非効率なのか」
データベース(InnoDB)の物理構造を想像してほしい。
WordPressの `wp_posts` テーブルには、標準でいくつかのインデックスが張られている。その代表が `type_status_date` 複合インデックス(※環境やバージョンにより異なるが、概ね `post_type`, `post_status`, `post_date` の順で構成されることが多い)だ。
単一ステータス(`post_status = ‘publish’`)の場合
MySQLは複合インデックスの左側から順番に条件を評価していく。
`post_type` で絞り込み、次に `post_status = ‘publish’` でダイレクトにB-Treeインデックスをヒットさせることができる(Index Range Scan)。そのため、数百万件のレコードがあってもミリ秒単位で結果が返る。
複数ステータス(`IN`句 や `OR` 条件)の場合
一方で、`post_status` に配列を指定したり `’any’` を指定したりすると、内部で `OR` 条件や `IN` 句が多用される。
MySQLのオプティマイザにとって、`OR` で結ばれた条件や複数のステータスをまたぐ検索は、インデックスを効率的にスキャンし続けることが難しくなる場合がある。結果として、インデックスが効かずにテーブル全体を舐め回す「フルテーブルスキャン(Full Table Scan)」に格下げされるリスクが跳ね上がるのだ。
特に、リビジョンやゴミ箱(`trash`)まで含める `’any’` の多用は、パフォーマンス上の最大の禁忌の一つである。
—
3. 実務で使える堅牢な設計パターン
では、開発現場においてどのようにコードを書くべきか。
「とりあえず動く」ではなく、「スケールしても耐えうる」プロダクションコードの書き方を提示する。
アンチパターン:不必要に広いステータスを指定する
// 【NG】全ステータスを取得しようとして ‘any’ を使う
// これはお勧めしない。不要なリビジョンや下書きまでメモリにロードし、DB負荷も増大する。
$bad_query = new WP_Query([
‘post_type’ => ‘custom_post’,
‘post_status’ => ‘any’,
]);
ベストプラクティス:必要なステータスを明示し、キャッシュ戦略を組み合わせる
どうしても公開済み以外のデータ(例えば、プレビューや下書きを含む管理画面用のカスタムAPIなど)を扱う必要がある場合は、ステータスを厳密に絞り込み、さらにトランジェントキャッシュやObject Cache(Redis/Memcached)を組み合わせるのが鉄則だ。
以下に、実務のコンポーネント設計でそのまま使える堅牢なラッパー関数のコードを示す。
/
- 堅牢性とパフォーマンスを考慮した投稿取得関数
- @param array $args カスタムクエリ引数
- @return WP_Post[] 投稿オブジェクトの配列
/
function my_optimized_get_posts( array $args = [] ): array {
// デフォルトでセキュリティとパフォーマンスを担保した引数を定義
$defaults = [
‘post_type’ => ‘post’,
‘post_status’ => ‘publish’, // 原則は ‘publish’ のみでインデックスを最優先
‘posts_per_page’ => 10,
‘no_found_rows’ => true, // ページネーションの総件数計算(SQL_CALC_FOUND_ROWS)を無効化し爆速化
‘update_post_meta_cache’ => false, // メタデータを使用しない場合はfalseにしてN+1問題を回避
‘update_post_term_cache’ => false, // ターム(タクソノミ)を使用しない場合も同様に切る
];
$parsed_args = wp_parse_args( $args, $defaults );
// キャッシュキーの生成(クエリ内容のハッシュ化)
$cache_key = ‘opt_posts_’ . md5( serialize( $parsed_args ) );
$cached_data = wp_cache_get( $cache_key, ‘my_custom_group’ );
if ( false !== $cached_data ) {
return $cached_data;
}
$query = new WP_Query( $parsed_args );
$posts = $query->posts;
// 1時間キャッシュを保持(データ更新時にパージする設計を別途入れること)
wp_cache_set( $cache_key, $posts, ‘my_custom_group’, HOUR_IN_SECONDS );
return $posts;
}
—
4. テクニカルリードからの総括
プログラミングとは、ただ動くコードを書くことではない。「リソースの限界を意識した選択の積み重ね」だ。
`WP_Query` における `post_status` の指定は、単なる条件分岐ではなく、MySQLのストレージエンジンのインデックス構造に直接ダイブする行為である。
次にコードを書くときは、脳内でこう自問してほしい。
> 「このクエリは、本当にインデックスをフル活用できているか?」
> 「`post_status` に余計なワイルドカードやマルチステータスを指定して、DBに余分な負荷をかけていないか?」
この視点を持てた瞬間から、君の書くコードの品質は一段上のステージへ到達する。
レビューは以上だ。修正版のプルリクエストを待っている。