【実務・中級編】初心者向け:WP_Queryで発行されるSQLをログ出力して「重いクエリ」を特定する最初の一歩 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

WP_Queryの内部発行クエリ最適化とインデックスチューニング

初心者向け:WP_Queryで発行されるSQLをログ出力して「重いクエリ」を特定する最初の一歩

開発プロジェクトのテクニカルリードとして、はっきりと言っておこう。
「なぜかサイトが重い」「ページネーションの2ページ目以降が極端に遅い」。その原因の9割は、`WP_Query` が裏側で吐き出している無駄で巨大なSQLにある。

WordPressは極めて優秀なCMSだが、その抽象化レイヤーの高さゆえに、開発者が意図しない複雑なSQL(特に `NOT IN`, `meta_query` の多用、`tax_query` の複数指定によるJOINの爆発)をいとも簡単に生み出してしまう。

データベースの内部構造、そしてWordPressがどのようにクエリを構築し発行しているのかを理解せずして、真のパフォーマンスチューニングは語れない。
今回は、その第一歩として `SAVEQUERIES` 定数を用いたクエリの可視化と、ボトルネックを炙り出すための実務的なアプローチを伝授する。

—

1. なぜ `WP_Query` の背後にあるSQLを知る必要があるのか?

コードレビューの際、「この `WP_Query` は遅いから書き直してくれ」と指摘すると、ジュニアエンジニアから「何がダメなんですか? 動いてますよ」という返答が返ってくることがある。

何がダメなのか。それをロジカルに証明するには、実際に発行されたSQLと実行時間、そしてコールスタック(どこから呼ばれたか)を突きつけるのが最も手っ取り早い。

WordPressのコア内部では、すべてのデータベースクエリが `$wpdb` クラスの `query()` メソッドを通過する。
ここでデバッグモードが有効化されている場合、発行されたSQL文はメモリ上に蓄積される仕組みになっている。これを利用するのが `SAVEQUERIES` だ。

—

2. `SAVEQUERIES` によるクエリ可視化の基本設計

まずは、環境変数または `wp-config.php` でクエリの記録を有効化する。
本番環境でこれを常時有効にするとメモリを喰いつぶすため、ステージング環境やローカル環境、あるいは特定の管理者権限を持つユーザーに限定して有効化するのがプロの設計だ。

以下のコードを `wp-config.php` または初期化フックの早い段階で定義する。

/

  • デバッグ環境または特定条件下でのみクエリ保存を有効化

/
if ( ! defined( ‘SAVEQUERIES’ ) ) {
// 本番環境以外、かつ管理者のみに絞るなどのガードを入れるのが堅牢
define( ‘SAVEQUERIES’, true );
}

—

3. 【プロダクションコード】重いクエリとコールスタックを炙り出すデバッグスニペット

単にSQLを出力するだけでは不十分だ。「どのテンプレートの、どのクエリが、何秒かかったのか」を特定できなければ、大規模なコードベースではデバッグのしようがない。

以下のコードは、フッター(`wp_footer`)または管理者バーに、実行された全SQL文、実行時間(ミリ秒)、およびそのクエリを発行したPHPのコールスタック(関数呼び出し履歴)を美しく出力するプロダクションクオリティのコードだ。

/

  • 開発環境・管理者向け:発行されたSQLクエリのパフォーマンス解析ツール
  • 設置場所: mu-plugins/ または テーマのfunctions.php

/
function my_debug_display_database_queries() {
// 権限チェック:管理者以外には絶対に露出させない
if ( ! current_user_can( ‘administrator’ ) ) {
return;
}

// SAVEQUERIESが有効になっていない場合は早期リターン
if ( empty( $GLOBALS[‘wp_queries’] ) && ( ! isset( $GLOBALS[‘wpdb’] ) || empty( $GLOBALS[‘wpdb’]->queries ) ) ) {
echo ‘

SAVEQUERIES is not enabled or no queries recorded.

‘;
return;
}

$queries = $GLOBALS[‘wpdb’]->queries;
$total_time = 0;
$total_queries = count( $queries );

echo ‘

‘;
echo ‘

WordPress Query Analyzer (Total: ‘ . $total_queries . ‘ queries)

‘;
echo ‘

‘;
echo ‘

‘;
echo ‘

‘;

foreach ( $queries as $index => $query ) {
// $query[0] = SQL, $query[1] = 実行時間(秒), $query[2] = コールスタック
$sql = $query[0];
$time = $query[1] 1000; // ミリ秒に変換
$total_time += $time;

// 実行時間が長い(例: 15ms以上)クエリを視覚的に警告する
$row_bg = ( $time > 15 ) ? ‘background:rgba(255,0,0,0.2);’ : ( $index % 2 === 0 ? ‘background:#252526;’ : ‘background:#1e1e1e;’ );

// コールスタックの整形(不要な内部関数を除外して可読性を上げる)
$stack = isset( $query[2] ) ? $query[2] : ‘N/A’;
$stack_lines = explode( ‘, ‘, $stack );
// 直近の自前コードを分かりやすくするためにフィルタリングまたはスライス
$filtered_stack = implode( ‘
‘, array_slice( $stack_lines, 0, 5 ) );

echo ‘

‘;
echo ‘

‘;
echo ‘

‘;
echo ‘

‘;
echo ‘

‘;
}

echo ‘

‘;
echo ‘

‘;
echo ‘

‘;
echo ‘

‘;
echo ‘

Time (ms) SQL Statement Caller Stack
‘ . number_format( $time, 2 ) . ‘ ms ‘ . esc_html( $sql ) . ‘ ‘ . wp_kses_post( $filtered_stack ) . ‘
Total Database Query Time: ‘ . number_format( $total_time, 2 . ‘ ms’ ) . ‘

‘;
echo ‘

‘;
}
add_action( ‘wp_footer’, ‘my_debug_display_database_queries’, 999 );

—

4. コードレビューの視点:何が「重いクエリ」の兆候か?

上記のツールを導入してログを出力した際、以下の兆候が見られたら即座にクエリの設計を見直す必要がある。

1. `NOT IN` や複数メタキーの `OR` 検索

  • `meta_query` で複数の条件を `relation => ‘OR’` で結んだり、存在しないカスタムフィールドの有無を判定するクエリは、MySQLのインデックスを完全に殺す(フルテーブルスキャンを引き起こす)。

2. `posts.post_content LIKE ‘%keyword%’` の嵐

  • 全文検索において `LIKE` 検索はデータ量が増えると一瞬でボトルネックになる。数万件以上のレコードがある場合は、MySQLのフルテキストインデックス(Natural Language Mode)や、Elasticsearch等の外部検索エンジンへのオフロードを検討すべきだ。

3. 不要な `JOIN`(`postmeta` の結合過多)

  • `WP_Query` に `meta_key` と `meta_value` を渡すと、WordPressは自動的に `wp_postmeta` テーブルをJOINする。検索条件に絡まないメタデータを取得するためだけにJOINしている場合は、オブジェクトキャッシュ(Redis/Memcached)やカスタムテーブルの設計を検討せよ。

—

5. テクニカルリードからの総括

データベースの最適化は、勘や経験則で行うものではない。
まずは今回紹介した `SAVEQUERIES` の仕組みを理解し、「どのSQLが何ミリ秒かかっているか」というファクト(事実)を直視することからすべてが始まる。

「動いているからヨシ」ではなく、「なぜこのSQLが発行されているのか」「インデックスは効いているのか(`EXPLAIN` の活用)」まで踏み込んでこそ、真にスケーラブルなWordPressアプリケーションを構築できる。次のスプリントでは、君のプロジェクトでも必ずクエリの可視化から始め給え。

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