【入門編】上級プロフェッショナル向け:WP_Queryの実行計画をリアルタイムで監視する「Slow Query Logger」の自作 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

こんにちは!WordPressの内部構造やパフォーマンス最適化の世界へようこそ。

今回は、他のプログラミング言語からWordPressに入ってきた方や、「そろそろデータベースの裏側の動きを完全に理解したい!」というステップアップ中のあなたに向けて、非常にエキサイティングなテーマをご用意しました。

それが、「WP_Queryの実行計画をリアルタイムで監視する『Slow Query Logger』の自作」です。

「データベースのクエリが遅いな……」となんとなく感じて放置していませんか? WordPressの心臓部である `WP_Query` は、非常に高機能ゆえに、使い方を誤ると巨大なSQLを暴発させ、サイト全体のパフォーマンスを致命的に低下させます。

今回は、一定ミリ秒を超えた重いクエリを自動で検知し、MySQLの実行計画(`EXPLAIN`)を添えてSlackへ通知するプロファイリングシステムを一緒に作っていきましょう。ここをクリアすれば、WordPressのデータベース周りの基本と仕組みはバッチリマスターできますよ!

—

なぜ `WP_Query` の監視が必要なのか?

WordPressのプラグインやテーマ開発で、データベースから投稿データを取得するとき、私たちは無意識に次のようなコードを書きますよね。

$query = new WP_Query([
‘post_type’ => ‘post’,
‘posts_per_page’ => 10,
]);

このシンプルに見えるコードの裏側で、WordPressは `posts` テーブルだけでなく、`postmeta` テーブルや `term_relationships` テーブルなどを複雑に結合(JOIN)し、条件分岐の多い巨大なSQLを組み立てて発行しています。

もし、記事数が数十万件に膨れ上がり、適切なインデックス(索引)が貼られていない環境でこれが実行されたらどうなるでしょうか? MySQLはテーブルの先頭から終わりまですべてを走査する「フルテーブルスキャン」を行い、CPU使用率が跳ね上がってしまいます。

だからこそ、「どのクエリが、どれだけの時間をかけ、なぜ遅いのか」をリアルタイムで把握する必要があるのです。

—

全体像:Slow Query Logger の仕組み

今回作成するシステムの流れは以下の通りです。

1. 検知: `WP_Query` が実行され、データベースから結果が返ってきた瞬間をフックする。
2. 計測: クエリの実行にかかった時間をミリ秒単位で計測する。
3. 判定: 設定した閾値(例: 50ms)を超えているか判定する。
4. 解析: 超えていた場合、MySQLに対して `EXPLAIN [発行されたSQL]` を実行し、データベースがどういう計画でデータを探したのかを取得する。
5. 通知: SQL文、実行時間、EXPLAINの結果をまとめて Slack に飛ばす。

イメージとしては、あなたのサイト専属のデータベース監視ガードマンを常駐させるようなものですね。

—

実装コード:Slow Query Logger の全貌

それでは、この監視システムをプラグイン、もしくはテーマの `functions.php` に実装していきましょう。初学者の方にも分かりやすいよう、細かくコメントを挟んでいます。

  • Plugin Name: Advanced Slow WP_Query Logger
  • Description: 一定時間以上かかったWP_Queryを検知し、EXPLAIN結果と共にSlackへ通知する
  • Version: 1.0.0
  • Author: 頼れる先輩エンジニア
  • /

    class Slow_WP_Query_Logger {

    // 処理時間を計測するための一時保存用変数
    private $start_time = 0;

    // 遅いと判定する閾値(ミリ秒)
    private $threshold_ms = 50;

    // SlackのWebhook URL(ご自身の環境に合わせて変更してください)
    private $slack_webhook_url = ‘https://hooks.slack.com/services/YOUR/WEBHOOK/URL’;

    public function __construct() {
    // WP_Queryの実行開始をフック
    add_action(‘pre_get_posts’, [$this, ‘start_timer’]);

    // WP_Queryのデータベース問い合わせ直後をフック(WordPress 4.6+で利用可能)
    add_filter(‘posts_request’, [$this, ‘measure_and_inspect’], 10, 2);
    }

    /

    • 1. クエリ実行前のタイムスタンプを記録

    /
    public function start_timer($query) {
    // 管理画面やAjaxなどを除外したい場合はここでガード条件を書きます
    if (is_admin()) {
    return;
    }
    $this->start_time = microtime(true);
    }

    /

    • 2. クエリ実行時間を測定し、閾値を超えていたら分析へ進む

    /
    public function measure_and_inspect($request, $query) {
    if (!$this->start_time || is_admin()) {
    return $request;
    }

    // 実行時間をミリ秒で計算
    $execution_time = (microtime(true) – $this->start_time) 1000;

    // タイマーをリセット
    $this->start_time = 0;

    // 閾値を超えている場合のみ処理を実行
    if ($execution_time > $this->threshold_ms) {
    $this->analyze_and_notify($request, $execution_time);
    }

    return $request;
    }

    /

    • 3. EXPLAINの取得とSlackへの通知

    /
    private function analyze_and_notify($sql, $execution_time) {
    global $wpdb;

    // MySQLの実行計画(EXPLAIN)を取得
    // EXPLAINは、データベースがどのようにインデックスを使い、何行スキャンしたかを教えてくれます
    $explain_results = $wpdb->get_results(“EXPLAIN {$sql}”, ARRAY_A);

    // 通知メッセージの組み立て
    $message = “⚠️ Slow WP_Query Detected!\n”;
    $message .= “• Execution Time: ” . round($execution_time, 2) . ” ms\n”;
    $message .= “• URL: ” . home_url($_SERVER[‘REQUEST_URI’]) . “\n”;
    $message .= “• SQL Query:\n{$sql}\n”;

    $message .= “• EXPLAIN Plan:\n”;
    foreach ($explain_results as $row) {
    $message .= sprintf(
    “table: %s | type: %s | possible_keys: %s | key: %s | rows: %s\n”,
    $row[‘table’] ?? ”,
    $row[‘type’] ?? ”,
    $row[‘possible_keys’] ?? ”,
    $row[‘key’] ?? ”,
    $row[‘rows’] ?? ”
    );
    }
    $message .= “”;

    // Slackへ非同期的に送信 (wp_remote_postを使用)
    wp_remote_post($this->slack_webhook_url, [
    ‘body’ => json_encode([‘text’ => $message]),
    ‘headers’ => [‘Content-Type’ => ‘application/json’],
    ‘blocking’ => false, // サイトの表示速度に影響を与えないよう非同期に
    ‘timeout’ => 5,
    ]);
    }
    }

    // クラスのインスタンス化
    new Slow_WP_Query_Logger();

    —

    コードの核心:ここが分かれば怖くない!

    初心者の方が「おっ」と躓きやすいポイントや、このコードの重要な技術的要素をいくつか解説しますね。

    ① `pre_get_posts` と `posts_request` の連携

    WordPressのフックの仕組みを理解する上で、このコンビネーションは非常に美しいです。

    • `pre_get_posts` は、`WP_Query` オブジェクトが生成され、まさにデータベースへクエリを投げに行く「直前」に発火します。ここで `microtime(true)` を使って正確なスタート時間を測ります。
    • `posts_request` は、SQL文そのものを書き換えたりフックしたりするためのフィルターフックです。ここでは「データベースから結果が返ってきた瞬間」に位置するため、スタート時間との引き算を行うことで「純粋なSQLの実行時間」をミリ秒単位で正確に割り出すことができます。

    ② `EXPLAIN` の読み解き方

    通知に含まれる `EXPLAIN` の結果の中で、特に以下のポイントに注目してください。

    • `type: ALL`: これは非常に危険なシグナルです。「フルテーブルスキャン(全件走査)」が行われていることを意味します。データが増えれば増えるほど遅くなります。
    • `key: NULL`: インデックス(索引)が全く使われていない状態です。
    • `rows:`: MySQLが目的のデータにたどり着くために、何行のデータをチェックしたかを表します。ここが数万・数十万になっている場合は、インデックスの追加やクエリの改善が必要です。

    ③ なぜ `blocking => false` なのか?

    Slackへの通知に `wp_remote_post` を使っていますが、その際の引数に `’blocking’ => false` を指定しています。
    これは、「Slackへの通信が終わるのをWordPressが待たなくていいよ(バックグラウンドで勝手に送っといてね)」という設定です。もしこれを `true` にしてしまうと、Slack側のサーバーの応答が遅いときに、あなたのWebサイト全体の表示速度が遅くなってしまうという本末転倒な事態が起きるので注意してくださいね。

    —

    陥りやすい文法・実装エラーと対策

    1. 無限ループや再帰的クエリの罠

    • エラー: `EXPLAIN` を実行するために `$wpdb->get_results()` を使っていますが、もしこの `get_results` 自体が別のフィルターフックを誘発してしまうと、無限ループ(クラッシュ)を起こす可能性があります。
    • 対策: 今回のコードでは管理画面(`is_admin()`)を除外していますが、プラグイン独自のデータベース操作が絡む場合は、フックの再帰的実行に十分注意してください。

    2. 変数のスコープ(`$this->start_time`)の共有

    • エラー: `WP_Query` は1つのページリクエストの中で何度も(メインクエリ、サイドバーのウィジェット、カスタムループなど)複数回実行されることがあります。
    • 対策: 単純な変数で保持すると、複数のクエリが同時に走ったときに計測がバッティングする可能性があります。完璧を期す場合は、クエリのハッシュ値をキーにして配列で時間を管理するとより堅牢になります。

    —

    まとめ

    いかがでしたでしょうか?
    今回は、単なるプラグインの使い方を超えて、WordPressの内部フックの実行順序、MySQLの実行計画(`EXPLAIN`)、そしてパフォーマンスを落とさないための非同期処理のテクニックまでを一気に解説しました。

    「なぜこのクエリが遅いのか?」を感覚ではなく、データ(EXPLAIN結果)を元に論理的に突き止められるようになると、あなたのエンジニアとしてのスキルは間違いなく一つ上のステージへと駆け上がります。

    ここをクリアできれば、もうWordPressのデータベース構造は怖くありません。ぜひご自身の開発環境やステージング環境で試してみてくださいね。それでは、快適なWordPress開発ライフを!

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