MySQL道普請便り

第276回EXPLAIN FOR CONNECTIONを使用して実行中のクエリの実行計画を取得する

皆さんは、本番環境でアプリケーションが発行したクエリの実行時間が急に長くなり、原因調査に困った経験はないでしょうか。

通常、このような場合はEXPLAINを実行して実行計画を確認します。しかし、問題となっているSQLはすでにアプリケーションから実行されており、SQL全文やバインド値を取得できなかったり、本番環境で同じ条件のSQLを再実行することが難しかったりするケースも少なくありません。

このような場面で役立つのがEXPLAIN FOR CONNECTIONです。EXPLAIN FOR CONNECTIONを利用すると、実行中のセッションに対して、そのSQLの実行計画を別セッションから取得できます。

本連載では過去にEXPLAINについて、第222回 EXPLAIN FORMATによるクエリ実行計画の出力の違いで紹介しました。本記事では、その中でも触れたEXPLAIN FOR CONNECTIONに焦点を当て、通常のEXPLAINとの違いや利用シーン、実際の使い方について解説します。

検証環境

今回はDocker上で起動したMySQLを使って確認します。以下のコマンドでMySQLを起動します。検証環境には、本稿執筆時点で最新のLTSであるMySQL 9.7を使用します。ちなみにEXPLAIN FOR CONNECTION自体はMySQL 5.7から利用可能な機能です。

docker run --name mysql97 --rm -d \
  -e MYSQL_ROOT_PASSWORD=password \
  mysql:9.7

以上で起動したMySQLコンテナに接続します。

$ docker exec -it mysql97 bash
bash-5.1# mysql -h127.0.0.1 -uroot -ppassword

いつものMySQLクライアントの画面が表示されれば準備完了です。

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 9.7.2 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

ちゃんと9.7.2になっています。こちらに以下のユーザーデータべースとテーブルを作成してみましょう。年齢と名前と作成日を持ったテーブルになります。

mysql> drop database users;
Query OK, 0 rows affected (0.033 sec)

mysql> create database users;
Query OK, 1 row affected (0.020 sec)

mysql> use users
Database changed
mysql> CREATE TABLE users (
    ->     id BIGINT PRIMARY KEY AUTO_INCREMENT,
    ->     age INT NOT NULL,
    ->     name VARCHAR(100),
    ->     created_at DATETIME,
    ->     INDEX idx_age(age)
    -> );
Query OK, 0 rows affected (0.058 sec)

続いてデータの投入を行います。

今回はデータ生成には再帰共通テーブル式(Recursive CTE)を利用し、1から100万までの連番を生成します。その連番をもとに、ランダムな年齢や名前、作成日時を持つデータをINSERT SELECT文で一括投入します。

なお、100万回の再帰を行うため、事前にcte_max_recursion_depthを100万へ変更しています。

SET SESSION cte_max_recursion_depth = 1000000;

INSERT INTO users(age, name, created_at)
WITH RECURSIVE seq AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1
    FROM seq
    WHERE n < 1000000
)
SELECT
    FLOOR(RAND()*100),
    CONCAT('user', n),
    NOW() - INTERVAL FLOOR(RAND()*365) DAY
FROM seq;

EXPLAIN FOR CONNECTIONを試してみる

EXPLAIN FOR CONNECTIONは、対象セッションで現在実行中のステートメントに対して、オプティマイザが生成した実行計画を取得します。そのため、SQLをコピーして再実行する必要がなく、アプリケーションが実際に発行しているクエリの実行計画をそのまま確認できます。試してみるためにはまず初めにconnectionのIDが必要になるので控えておきましょう。

mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
|               9 |
+-----------------+
1 row in set (0.001 sec)

本稿では9であることがわかったので、EXPLAIN FOR CONNECTION 9;と実行すれば取得できることがわかります。そのままターミナルで実行すると、以下のようなエラーが発生します。

mysql> EXPLAIN FOR CONNECTION 9;
ERROR 3012 (HY000): EXPLAIN FOR CONNECTION command is supported only for SELECT/UPDATE/INSERT/DELETE/REPLACE

今回は、SELECT/UPDATE/INSERT/DELETE/REPLACE以外となるEXPLAIN FOR CONNECTIONに対して実施しようとしたので、エラーになってます。

他のターミナルを起動して試してみましょう。

mysql> EXPLAIN FOR CONNECTION 9;
Query OK, 0 rows affected (0.001 sec)

このように何も実行していない場合は、対象がないため0件になります。

このように、EXPLAIN FOR CONNECTION はすべてのセッションに対して利用できるわけではありません。対象セッションがSleep状態の場合や、クエリが既に終了している場合は取得できません。また、短時間で終了するSQLは EXPLAIN FOR CONNECTION を実行する前に終了してしまうため、確認できないことがあります。そのため、本機能は長時間実行されているSQLの調査に特に有効です。

CONNECTION_IDが9のターミナルに戻って、以下のクエリ実行してみましょう。

SELECT id, name, SLEEP(1) FROM users LIMIT 50;

このクエリでは50行取ってきます。indexが効かないクエリとはいえ一瞬で返されてしまうので、今回はSLEEP関数を挟みます。そのため実際は50秒かかります。

その間にもう1つのターミナルを立ち上げて、そこで以下のクエリを実行してみましょう。

mysql> EXPLAIN FOR CONNECTION 9;
+---------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                           |
+---------------------------------------------------------------------------------------------------+
| -> Limit: 50 row(s)  (cost=100399 rows=50)
    -> Table scan on users  (cost=100399 rows=996052)
+---------------------------------------------------------------------------------------------------+
1 row in set (0.001 sec)

実際の調査での使い方

実際に本番環境でいざ調査という段階に、対象となるConnection IDがわかっているとは限りません。そのため、まずSHOW PROCESSLISTINFORMATION_SCHEMA.PROCESSLISTなどを利用して、長時間実行されているクエリとConnection IDを特定しましょう。

mysql> SHOW PROCESSLIST;
+----+-----------------+-----------------+-------+---------+------+------------------------+-----------------------------------------------+
| Id | User            | Host            | db    | Command | Time | State                  | Info                                          |
+----+-----------------+-----------------+-------+---------+------+------------------------+-----------------------------------------------+
|  5 | event_scheduler | localhost       | NULL  | Daemon  | 4348 | Waiting on empty queue | NULL                                          |
|  9 | root            | 127.0.0.1:59202 | users | Query   |   10 | User sleep             | SELECT id, name, SLEEP(1) FROM users LIMIT 50 |
| 11 | root            | localhost       | NULL  | Query   |    0 | init                   | SHOW PROCESSLIST                              |
+----+-----------------+-----------------+-------+---------+------+------------------------+-----------------------------------------------+
3 rows in set, 1 warning (0.017 sec)

上記で対象を見つけたら、そのConnection IDを指定して前述の通りEXPLAIN FOR CONNECTIONを実行できます。

EXPLAIN FOR CONNECTION ID;

なお、PROCESS権限を持つユーザーは任意の接続を指定できます。PROCESS権限を持たない場合は、自分のユーザーに属する接続のみ指定できます。また、対象のクエリをEXPLAINするために必要な権限も必要です。

この機能で確認できるのは、実行中のクエリに使用されている実行計画です。現在何行目まで処理が進んでいるか、どの処理で待機しているかといった実行途中の進捗まではわからないため、必要に応じてPerformance Schemaなどと組み合わせて調査しましょう。

まとめ

EXPLAIN FOR CONNECTION はMySQL 5.7から利用できる機能ですが、普段の開発では利用する機会が少ないため、存在を知らない方も多いかもしれません。しかし、本番環境で長時間実行されているSQLを調査する際には非常に有効な機能です。

EXPLAIN ANALYZEと違ってSQLを再実行する必要がなく、実際に実行中のクエリに対する実行計画を確認できるため、問題解決につながることも多いので覚えておくと役立つでしょう。ぜひ一度試してみてください。

おすすめ記事

記事・ニュース一覧