【MySQL】EXPLAINの読み方とSLOW QUERYの改善方法|インデックスチューニング実践

「このクエリ、なんでこんなに遅いんだろう…」

本番で突然レスポンスが遅くなり、ログを漁っていたら数秒かかっているSQLを発見した、という経験は誰でも一度はあるはずです。

MySQLのチューニングで最初に使うのが EXPLAIN コマンドです。出力を正しく読み解けるようになれば、インデックスが効いていない原因を即座に特定し、改善の見通しを立てることができます。

この記事では EXPLAIN の各カラムの意味と読み方、そしてスロークエリの特定から改善までの実践的な手順を、具体的なSQL例を交えて解説します。

EXPLAINとは

EXPLAIN は、MySQLがSQLをどのように実行するか(実行計画)を確認するためのコマンドです。SELECT 文の前に付けるだけで使えます。

EXPLAIN SELECT * FROM orders WHERE user_id = 123;

出力は1行以上のテーブル形式で返ってきます。JOINが増えると行数も増えます。

EXPLAINの出力カラムを読む

id

実行順序の識別子です。値が大きいほど先に実行されます。サブクエリがある場合は複数のidが登場します。

select_type

クエリの種類を示します。よく見るものは以下のとおりです。

意味
SIMPLEサブクエリ・UNIONなしの単純なSELECT
PRIMARY最外側のSELECT
SUBQUERYWHERE句内のサブクエリ
DERIVEDFROM句内のサブクエリ(派生テーブル)
UNIONUNION以降のSELECT

type(最重要)

テーブルへのアクセス方法を示します。パフォーマンスに直結する最重要カラムです。

説明評価
systemテーブルに行が1件のみ最速
constPRIMARY KEYまたはUNIQUEで1件特定最速
eq_refJOINでPRIMARY KEYを使って1件特定速い
refインデックスを使って複数行取得良好
rangeインデックスを使った範囲検索(BETWEEN、IN等)許容範囲
indexインデックスフルスキャン遅い
ALLフルテーブルスキャン最悪

ALL が出ている場合は要改善のサインです。大きなテーブルで ALL が出ていれば、確実にボトルネックになります。

possible_keys

MySQLが使用を検討したインデックスの一覧です。ここに候補が出ていても、実際に使われるとは限りません。

key

実際に選択されたインデックスです。NULL の場合、インデックスが一切使われていません。

key_len

使用されたインデックスのバイト長です。複合インデックスの場合、何カラム目まで使われたかをここで判断できます。

rows

MySQLが処理すると見積もった行数です。あくまで統計情報をもとにした推計値ですが、この値が大きいほどコストが高いことを意味します。

Extra

実行時の補足情報です。以下が出ていたら注意が必要です。

意味
Using filesortインデックスを使わずにソートしている(ORDER BYが非効率)
Using temporary一時テーブルを作成している(GROUP BY等)
Using indexカバリングインデックスで完結している(良いサイン)
Using whereWHERE条件でフィルタリングしている

Using filesortUsing temporary が重なっていると特にパフォーマンスへの影響が大きくなります。

スロークエリログで対象を絞る

EXPLAINを使う前に、まず問題のあるSQLを特定する必要があります。MySQLにはスロークエリログという機能があります。

スロークエリログの有効化

-- 現在の設定確認
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 動的に有効化(再起動不要)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 1秒以上かかるクエリを記録
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

本番環境では long_query_time を最初は 1(1秒)に設定し、チューニングが進んだら 0.50.1 に引き下げていくのがおすすめです。

mysqldumpslow でログを集計

# 実行時間の合計が長いクエリTop10
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 実行回数が多いクエリTop10
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

ここで出てきたSQLに対して EXPLAIN をかけるのが実践的な流れです。

EXPLAINを使った改善の実例

ケース1:フルテーブルスキャンをインデックスで解消

-- 改善前
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01';
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | rows | Extra  |             |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | 500000 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

type: ALLkey: NULLrows: 500000 ── フルスキャンが確定しています。

-- インデックスを追加
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

-- 改善後
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01';
+----+-------------+--------+-------+--------------------+--------------------+---------+------+------+-----------------------+
| id | select_type | table  | type  | possible_keys      | key                | key_len | rows | Extra                 |
+----+-------------+--------+-------+--------------------+--------------------+---------+------+------+-----------------------+
|  1 | SIMPLE      | orders | range | idx_status_created | idx_status_created | 6       |  120 | Using index condition |
+----+-------------+--------+-------+--------------------+--------------------+---------+------+------+-----------------------+

typerange になり、rows も大幅に削減されました。

ケース2:Using filesort を複合インデックスで解消

ORDER BY のカラムがインデックスに含まれていない場合、Using filesort が発生します。

EXPLAIN SELECT * FROM articles WHERE category_id = 5 ORDER BY published_at DESC LIMIT 20;
Extra: Using where; Using filesort

category_id 単体のインデックスしかない場合、ORDER BY に対応できません。複合インデックスで解消できます。

ALTER TABLE articles ADD INDEX idx_category_published (category_id, published_at);

インデックスのカラム順は WHERE 句のカラムを先に、ORDER BY のカラムを後ろに置くのが基本です。

ケース3:インデックスが存在するのに使われない

possible_keys にインデックス名が出ているのに keyNULL のケースがあります。主な原因は以下のとおりです。

型の不一致

-- user_id がINT型なのに文字列で比較
WHERE user_id = '123'  -- インデックス無効
WHERE user_id = 123    -- インデックス有効

関数の使用

-- カラムに関数を使うとインデックスが無効になる
WHERE DATE(created_at) = '2024-01-01'  -- NG
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02'  -- OK

LIKE の前方一致以外

WHERE name LIKE '%田中%'   -- フルスキャン
WHERE name LIKE '田中%'    -- インデックス使用可

EXPLAIN ANALYZEで実際の実行時間を計測する(MySQL 8.0以降)

MySQL 8.0からは EXPLAIN ANALYZE を使うと、実際に実行した上での計測値が得られます。

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;

出力にはツリー形式で actual time(実測値)と rows(実際の行数)が含まれます。見積もりとの差が大きい場合は統計情報が古くなっている可能性があり、ANALYZE TABLE で更新できます。

ANALYZE TABLE orders;

チューニングの進め方まとめ

実務でのスロークエリ改善は、以下の流れで進めると効率的です。

  1. スロークエリログで対象SQLを特定する
  2. EXPLAIN で実行計画を確認する
  3. type: ALL または key: NULL の箇所を探す
  4. ExtraUsing filesort / Using temporary がないか確認する
  5. インデックスを追加または見直す
  6. 再度 EXPLAIN で改善を確認する
  7. 本番適用前にステージング環境で実測する

インデックスは追加しすぎるとINSERT/UPDATEのコストが増加するため、本当に必要なものだけに絞ることも重要です。不要なインデックスは DROP INDEX で整理しましょう。

まとめ

EXPLAIN の出力で最初に確認すべきは typekey の2カラムです。type: ALLkey: NULL の組み合わせはフルスキャンの証拠であり、インデックス追加の最優先候補となります。

スロークエリログと EXPLAIN を組み合わせることで、勘に頼らずデータに基づいたチューニングが可能になります。MySQL 8.0環境では EXPLAIN ANALYZE も積極的に活用してください。

DatabaseMySQL

Posted by 千原 耕司