EXPLAINで実行計画を読む
スロークエリを発見したら、次は「なぜ遅いのか」を調べます。そのための最強ツールが EXPLAIN? MySQLがSQLをどのように実行するかの「実行計画」を表示するコマンド。インデックスが使われているか、何行スキャンしているかなどを確認でき、チューニングの出発点になる。 です。EXPLAIN はクエリの前に付けるだけで、MySQL がそのクエリをどのように実行しようとしているか——どのインデックスを使うか、何行スキャンするか——を表示してくれます。
EXPLAIN の基本的な使い方
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
これだけです。SELECT の前に EXPLAIN を付けるだけで実行計画が得られます。
各列の意味
id
クエリのステップ番号です。単純なクエリは1つだけですが、サブクエリや UNION があると複数の行が現れます。同じ id を持つ行は同じステップで実行されます。
select_type
クエリの種類です。
| 値 | 意味 |
|---|---|
| SIMPLE | サブクエリも UNION もない単純な SELECT |
| PRIMARY | 外側のクエリ |
| SUBQUERY | サブクエリ |
| DERIVED | FROM 句のサブクエリ(derived table) |
type(最重要)
インデックスの使われ方を表す最も重要な列です。
key と key_len
key は実際に使われたインデックス名、key_len はインデックスのうち何バイト使ったかです。複合インデックスでどのカラムまで使われたかを key_len から推測できます。
rows
MySQL が返すと推定している行数です。実際の行数ではなくあくまで統計情報からの推定ですが、100行と100万行では意味がまったく異なります。
Extra
クエリの実行方法に関する追加情報です。
| 値 | 意味 |
|---|---|
| Using index | インデックスだけで完結(テーブルアクセス不要) |
| Using where | WHERE による絞り込みが発生 |
| Using filesort | ORDER BY のソートにインデックスを使えなかった |
| Using temporary | 一時テーブルが作成された(GROUP BY など) |
Using filesort や Using temporary はパフォーマンス上の警告サインです。
type の読み方
type 列は左が良く、右に行くほど悪化します。
const > eq_ref > ref > range > index > ALL
const / eq_ref(最良)
プライマリキーまたはユニークインデックスへの等値検索。1行だけが確定します。
-- type: const(プライマリキーで1行確定)
EXPLAIN SELECT * FROM users WHERE id = 42;
ref
ユニークでないインデックスへの等値検索。複数行がヒットする可能性があります。
-- type: ref(user_idにインデックスがある場合)
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
range
インデックスを使った範囲検索。BETWEEN、>、<、IN などが該当します。
-- type: range
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-01';
index
インデックス全体をスキャン。テーブル全スキャンよりはましですが、大量行では遅くなります。
ALL(最悪)
フルテーブルスキャン? インデックスを使わずにテーブルの全行を順番に読み込む操作。少量データなら問題ないが、大きなテーブルでは深刻なパフォーマンス問題になる。EXPLAINのtypeが`ALL`の場合がこれ。 ——テーブルのすべての行を読む最も非効率な方法です。行数が多いテーブルで type: ALL が出たら要注意です。
-- インデックスなし、type: ALL
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
EXPLAIN ANALYZE(MySQL 8.0以降)
MySQL 8.0.18から使える EXPLAIN ANALYZE は、推定値だけでなく実際の実行統計も表示します。
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
出力例:
-> Index lookup on orders using idx_user_id (user_id=100)
(cost=4.51 rows=12) (actual time=0.082..0.124 rows=15 loops=1)
rows=12(推定)と rows=15(実際)の乖離が大きいほど、統計情報が古くなっているサインです。そのときは ANALYZE TABLE orders; で統計を更新しましょう。
実例:スロークエリを EXPLAIN で分析する
前回のエピソードで発見したスロークエリを分析してみます。
-- 問題のクエリ
SELECT * FROM orders WHERE status = 'pending';
EXPLAIN SELECT * FROM orders WHERE status = 'pending'\G
* 1. row *
id: 1
select_type: SIMPLE
table: orders
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 845231
Extra: Using where
type: ALL、key: NULL、rows: 845231——典型的なフルスキャンです。status カラムにインデックスがないため、84万行すべてを読んでいます。
改善策としてインデックスを追加します。
ALTER TABLE orders ADD INDEX idx_status (status);
再度 EXPLAIN を実行すると:
type: ref
possible_keys: idx_status
key: idx_status
key_len: 1023
rows: 2341
Extra: Using index condition
rows が84万から2341に激減しました。実行時間も3.5秒から数ミリ秒に短縮されます。
EXPLAIN を読む習慣をつけることで、インデックスの効果を事前に確認してからデプロイできます。次のエピソードでは、そのインデックスの仕組み—— B-treeインデックス? MySQLのデフォルトのインデックス構造。バランス木(Balanced Tree)で値を管理し、等値検索・範囲検索・ORDER BY の高速化に有効。ほとんどのユースケースでこれを使う。 ——を詳しく見ていきます。