※この記事にはアフィリエイト広告(プロモーション)が含まれます。
SQL が遅いときは先頭に EXPLAIN を付けて、MySQL がどの索引を使ってどれだけの行を読むつもりかを見ます。この記事の出力は MySQL 8.4.11(Ubuntu 26.04 の標準パッケージ)で、30万行の orders 表を使って採りました。
まず見る(結論)
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 291997 | 10.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
typeがALL、keyがNULLなら表を全部読んでいるrowsは読む行数の見積もり。返す行数ではないpossible_keysに索引名があるのにkeyがNULLなら、索引を使えていない。直後にSHOW WARNINGSを見る
見る列
| 列 | 意味 |
|---|---|
type |
読み方。const・ref・range は索引で絞っている。index は索引を全部、ALL は表を全部読む |
possible_keys |
使える候補の索引 |
key |
実際に選んだ索引 |
key_len |
索引のうち使ったバイト数。複合索引で何列目まで使ったかの目安 |
rows |
読む行数の見積もり |
filtered |
読んだ行のうち条件に残る割合の見積もり(%) |
Extra |
補足。下の表を参照 |
GROUP BY customer_id では type が index、rows が 291997 でした。
列が多くて横に長いときは、末尾の ; を \G に変えると1列1行で縦に出ます。
索引を作って比べる
CREATE INDEX idx_cust ON orders (customer_id);
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| 1 | SIMPLE | orders | NULL | ref | idx_cust | idx_cust | 4 | const | 30 | 100.00 | NULL |
type が ref、key が idx_cust、rows が 291997 から 30 になりました。
EXPLAIN ANALYZE
EXPLAIN ANALYZE は実際に実行して、かかった時間と行数を出します。索引を作る前後で比べました。
-- 作る前
-> Filter: (orders.customer_id = 42) (cost=29448 rows=29200) (actual time=1.19..53.1 rows=30 loops=1)
-> Table scan on orders (cost=29448 rows=291997) (actual time=0.13..46.2 rows=300000 loops=1)
-- 作った後
-> Index lookup on orders using idx_cust (customer_id=42) (cost=10.5 rows=30) (actual time=0.0751..0.0776 rows=30 loops=1)
左の rows が見積もり、actual の中の rows が実際の行数、actual time はミリ秒です。
EXPLAIN FORMAT=TREE は同じ木の形で、実行はせず見積もりだけを出します。既定の形式は変数 explain_format で決まり、この環境では TRADITIONAL(表形式)でした。
索引があるのに使われない
文字列の列を数値で比べる
code は varchar(20) で、索引 idx_code があります。
EXPLAIN SELECT * FROM orders WHERE code = 'C12345'; -- type=ref, key=idx_code, rows=1
EXPLAIN SELECT * FROM orders WHERE code = 12345; -- type=ALL, possible_keys=idx_code, key=NULL, rows=291997
SHOW WARNINGS;
| Warning | 1739 | Cannot use ref access on index 'idx_code' due to type or collation conversion on field 'code' |
引用符を付け忘れただけで全件読みになり、エラーにもなりません。
列を関数で包む
条件(索引は created) |
type | key | rows |
|---|---|---|---|
DATE(created) = '2026-03-01' |
ALL | NULL | 291997 |
DATE_FORMAT(created, '%Y-%m') = '2026-03' |
ALL | NULL | 291997 |
created >= '2026-03-01' AND created < '2026-03-02' |
range | idx_created | 1440 |
関数で包むと possible_keys にも出ません。日付で絞るときは列をそのままにして範囲で書きます。
LIKE と、該当行が多い条件
code LIKE 'C1234%' は range(rows=111)、code LIKE '%2345' は ALL でした。先頭が決まっていない LIKE は索引で絞れません。
status の索引は、30万行中27万行が当たる status = 'done' でも ref で選ばれ、rows は 145998 でした。見積もりが実際(270000)とずれています。索引が使われていても rows が大きければ速くはなりません。
Extra に出るもの
| Extra | 意味 | 出た SQL |
|---|---|---|
Using where |
読んだ行を WHERE で絞っている | 索引の無い列の条件 |
Using index |
索引だけで答えが出て、表本体を読んでいない | SELECT COUNT(*) ... WHERE status = 'done' |
Using index condition |
索引の段階で条件を判定している | created の範囲検索 |
Using filesort |
索引の順番を使えず並べ替えている | ORDER BY amount DESC LIMIT 5、WHERE customer_id = 42 ORDER BY created |
Using temporary |
一時表を作っている | GROUP BY amount(索引なし) |
結合
EXPLAIN SELECT c.name, o.amount FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE c.name = 'cust42';
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| 1 | SIMPLE | c | NULL | ALL | PRIMARY | NULL | NULL | NULL | 10375 | 10.00 | Using where |
| 1 | SIMPLE | o | NULL | ref | idx_cust | idx_cust | 4 | fsv.c.id | 30 | 100.00 | NULL |
上の行から順に読みます。customers を全部読んで name で絞り、見つかった行ごとに orders を idx_cust で引いています。ref 列の fsv.c.id は、前の表の値で索引を引いているという意味です。EXPLAIN ANALYZE では、全体 2.33 ms のうち 2.25 ms が customers の全件読みでした。
UPDATE・DELETE と EXPLAIN
EXPLAIN UPDATE は計画を表示するだけで、値は変わりませんでした(type は range、key は PRIMARY)。
EXPLAIN ANALYZE UPDATE orders SET amount = amount + 100 WHERE id = 1;
-> <not executable by iterator executor>
EXPLAIN ANALYZE は UPDATE と DELETE のどちらでも上の1行が返るだけで、値も行も変わりませんでした。更新文の実際の時間は測れないので、同じ WHERE の SELECT で EXPLAIN ANALYZE します。
関連: MySQLのスロークエリログ / MySQLのALTER TABLE / PostgreSQLの実行計画の見方(EXPLAIN)