WP_Queryの「s」パラメータが引き起こすデータベースの悲劇:LIKE検索の限界とインデックス最適化の極限知見
チーフシステムアーキテクトの視点から言わせてもらえば、WordPressの `WP_Query` における `s` パラメータ(キーワード検索)ほど、その手軽さの裏でデータベースサーバーのCPUとI/Oを静かに、確実に殺していく機能はない。
「検索機能を追加して」というクライアントの要求に対し、素朴に `new WP_Query( array( ‘s’ => $keyword ) )` を実装する開発者は後を絶たない。しかし、その裏でMySQLのオプティマイザが何を行っているか、ストレージエンジンがどれだけのペナルティを支払っているかを直視したことがあるだろうか。
本稿では、なぜWordPressのデフォルト検索がスケールしないのか、その低レイヤのメカニズムを解剖し、インデックスチューニングと検索エンジンのオフロードによる根本的な解決策を提示する。
—
1. なぜ `s` パラメータは遅いのか?:B-Treeインデックスの完全破壊
WordPressのデフォルト検索は、生成されるSQLを見れば一目瞭然だ。`wp_posts` テーブルの `post_title`、`post_content`、`post_excerpt` に対し、容赦なく `LIKE` 演算子とワイルドカード(`%`)を適用する。
実際に発行されるクエリの構造を抽象化して見てみよう。
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
WHERE 1=1
AND (((wp_posts.post_title LIKE ‘%keyword%’)
OR (wp_posts.post_excerpt LIKE ‘%keyword%’)
OR (wp_posts.post_content LIKE ‘%keyword%’)))
AND wp_posts.post_type = ‘post’
AND (wp_posts.post_status = ‘publish’)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
先頭一致と中間一致の決定的な差
データベースのインデックス(通常はB-Tree構造)は、データの順序を保持することで $O(\log N)$ の高速な検索を実現する。
- 前方一致検索 (`LIKE ‘keyword%’`): インデックスのツリーを辿り、該当する値の範囲を特定できるため、インデックスが有効に機能する(Range Scan)。
- 中間一致・後方一致検索 (`LIKE ‘%keyword%’` / `LIKE ‘%keyword’`): 検索パターンの先頭にワイルドカードが存在するため、B-Treeのどのブランチから探索を始めればいいか判断できない。結果として、データベースエンジンはフルテーブルスキャン(Full Table Scan: ALL)を強制される。
数百万件のレコードを持つ `wp_posts` テーブルにおいて、フルテーブルスキャンが走るということは、ディスク(あるいはバッファプール)上のすべてのデータ行をメモリ上にロードし、CPUが1行ずつ文字列のスキャンを行うことを意味する。これが「検索が遅い」根本原因である。
—
2. `SQL_CALC_FOUND_ROWS` という隠れたパフォーマンスキラー
さらに事態を悪化させているのが、古いバージョンのMySQL/MariaDBとの互換性やページネーションのためにWordPressが長年デフォルトで発行し続けてきた `SQL_CALC_FOUND_ROWS` だ。
上記のクエリの先頭にあるこの修飾子は、MySQLに対して「LIMIT句を無視して、条件に合致する総行数を計算しろ」と命令する。
内部実行計画(EXPLAIN)の崩壊
オプティマイザは、`LIMIT` が指定されていれば本来は最初のマッチング行が見つかった時点でスキャンを打ち切ることができる(Short-circuit evaluation)。しかし、`SQL_CALC_FOUND_ROWS` があると、クエリ結果が何件であれ、条件に一致するすべてのレコードを最後までスキャンし続ける。
これに `LIKE ‘%…%’` が組み合わさることで、データベースサーバーのI/O帯域とCPUキャッシュは完全に飽和する。これが、データ量が増えるにつれてWordPressサイトのレスポンスが非線形に悪化するメカニズムだ。
—
3. 現場で使える最適化アプローチ:コードによる介入
コアの挙動を理解した上で、このボトルネックをどう回避するか。シニアエンジニアが実務で採用すべきアプローチをコードベースで解説する。
アプローチ A: `SQL_CALC_FOUND_ROWS` の無効化とページネーションの最適化
まず、不要な総件数計算を排除する。WordPress 4.6以降、`no_found_rows => true` を指定することでこれを抑制できる。検索結果の厳密なページネーションが不要(あるいは「100件以上」のような曖昧な表示で十分)な場合、この設定は必須である。
/
- WP_Queryのパフォーマンスを極限まで高めるための引数構築
/
$args = array(
‘s’ => ‘最適化’,
‘post_type’ => ‘post’,
‘post_status’ => ‘publish’,
‘posts_per_page’ => 10,
// 【重要】SQL_CALC_FOUND_ROWSを排除し、フルスキャンコストを削減
‘no_found_rows’ => true,
);
$query = new WP_Query( $args );
アプローチ B: `posts_search` フィルターによるLIKEの排除(プレフィックス検索への置換)
どうしてもMySQLだけで処理し、かつパフォーマンスを落としたくない場合、検索仕様を「部分一致(中間一致)」から「前方一致」に割り切ることで、インデックスを強制的にヒットさせることが可能だ。
以下のコードは、`posts_search` フィルターフックを介入させ、デフォルトの中間一致を前方一致へ書き換える高度なテクニックである。
/
- デフォルトのLIKE ‘%keyword%’ を LIKE ‘keyword%’ に置換し、
- インデックス(Prefix Index / B-Tree)を有効化する
/
function optimize_wordpress_search_to_prefix( $search, $wp_query ) {
// 管理画面やメインクエリ以外、あるいは検索キーワードがない場合はスキップ
if ( is_admin() || ! $wp_query->is_search() || empty( $wp_query->query_vars[‘s’] ) ) {
return $search;
}
global $wpdb;
$keyword = $wp_query->query_vars[‘s’];
// プレースホルダーを安全にエスケープ(ワイルドカードは末尾のみに配置)
$like = $wpdb->esc_like( $keyword ) . ‘%’;
// post_title に対する前方一致検索のみに絞り込むことで、インデックスの効力を最大化する
// ※実務では必要に応じて post_content などを外す、あるいはフルテキストインデックスへ移行する
$search = $wpdb->prepare(
” AND ({$wpdb->posts}.post_title LIKE %s)”,
$like
);
return $search;
}
add_filter( ‘posts_search’, ‘optimize_wordpress_search_to_prefix’, 10, 2 );
注意: このアプローチはパフォーマンスを劇的に改善するが、「文言の途中に含まれるキーワード」にはヒットしなくなるため、ビジネス要件とのトレードオフを慎重に評価する必要がある。
—
4. 本格的なスケーラビリティ:MySQL Fulltext Search(全文検索)への移行
数万件を超える投稿を抱えるシステムにおいて、`LIKE` 検索にしがみつくのはアーキテクチャの敗北を意味する。MySQL 5.6以降(InnoDBストレージエンジン)およびMariaDBでは、FULLTEXTインデックスがサポートされている。
WordPressでこれをネイティブに活用するには、データベースレベルでのインデックス追加と、`posts_search` または `posts_clauses` を書き換えて `MATCH() AGAINST()` 構文を生成するカスタムロジックが必要となる。
— 事実行うべきMySQL側のインデックス追加の例(InnoDB)
ALTER TABLE wp_posts ADD FULLTEXT INDEX fx_title_content (post_title, post_content);
このインデックスが存在する場合、クエリは以下のように書き換わるべきだ。
SELECT ID FROM wp_posts
WHERE MATCH(post_title, post_content) AGAINST(‘+WordPress +Optimization’ IN BOOLEAN MODE)
AND post_type = ‘post’ AND post_status = ‘publish’;
これにより、リレーショナルデータベースの枠内でありながら、インデックス駆動型の高速な全文検索を実現できる。
—
5. 究極の解:外部検索エンジン(Elasticsearch / Algolia)の導入
エンタープライズ領域において、MySQLの全文検索ですら限界を迎えるフェーズが存在する。形態素解析(日本語の単語分割)、あいまい検索、類義語展開、そしてミリ秒単位の応答速度を求める場合、WordPressのデータベースレイヤから検索責務を完全に切り離すべきだ。
- Elasticsearch (または OpenSearch)
- Algolia
これらを WP REST API や専用プラグイン(ElasticPressなど)を介して統合し、`WP_Query` の実行そのものを外部検索エンジンのAPIコールへとバイパスする。
データベースは「コンテンツの永続化レイヤ」に徹し、検索という高負荷なCPU・メモリバウンドな処理を専用の分散検索エンジンにオフロードする――これこそが、大規模WordPressアーキテクチャにおける唯一無二の正解である。
—
まとめ
WordPressの `s` パラメータが遅いという現象は、単なる「仕様」ではなく、リレーショナルデータベースにおける文字列検索の物理的制約に起因する。
1. 現状把握: デフォルトの `LIKE ‘%…%’` と `SQL_CALC_FOUND_ROWS` がインデックスを破壊し、フルスキャンを引き起こしている事実をコードレベルで理解する。
2. 戦術的最適化: `no_found_rows` の活用や、前方一致へのクエリ書き換えで一時的な延命を図る。
3. 戦略的進化: MySQLのFULLTEXTインデックス、あるいはElasticsearch等の外部エンジンへのオフロードにより、システム全体のスケーラビリティを担保する。
エンジニアとして、フレームワークが用意したブラックボックスの中身を暴き、リソースの限界を突破する設計を構築してほしい。