【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 |
SUBQUERY | WHERE句内のサブクエリ |
DERIVED | FROM句内のサブクエリ(派生テーブル) |
UNION | UNION以降のSELECT |
type(最重要)
テーブルへのアクセス方法を示します。パフォーマンスに直結する最重要カラムです。
| 値 | 説明 | 評価 |
|---|---|---|
system | テーブルに行が1件のみ | 最速 |
const | PRIMARY KEYまたはUNIQUEで1件特定 | 最速 |
eq_ref | JOINで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 where | WHERE条件でフィルタリングしている |
Using filesort と Using 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.5 や 0.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: ALL、key: NULL、rows: 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 |
+----+-------------+--------+-------+--------------------+--------------------+---------+------+------+-----------------------+
type が range になり、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 にインデックス名が出ているのに key が NULL のケースがあります。主な原因は以下のとおりです。
型の不一致
-- 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;
チューニングの進め方まとめ
実務でのスロークエリ改善は、以下の流れで進めると効率的です。
- スロークエリログで対象SQLを特定する
EXPLAINで実行計画を確認するtype: ALLまたはkey: NULLの箇所を探すExtraにUsing filesort/Using temporaryがないか確認する- インデックスを追加または見直す
- 再度
EXPLAINで改善を確認する - 本番適用前にステージング環境で実測する
インデックスは追加しすぎるとINSERT/UPDATEのコストが増加するため、本当に必要なものだけに絞ることも重要です。不要なインデックスは DROP INDEX で整理しましょう。
まとめ
EXPLAIN の出力で最初に確認すべきは type と key の2カラムです。type: ALL と key: NULL の組み合わせはフルスキャンの証拠であり、インデックス追加の最優先候補となります。
スロークエリログと EXPLAIN を組み合わせることで、勘に頼らずデータに基づいたチューニングが可能になります。MySQL 8.0環境では EXPLAIN ANALYZE も積極的に活用してください。


