MySQL 12 min read

MySQLのEXPLAINの見方 ― type・key・rows・Extraで索引が効いているかを確かめる

※この記事にはアフィリエイト広告(プロモーション)が含まれます。

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)

MySQL をもっと深く知るための本

MySQL徹底入門 第5版 MySQL 8.4 LTS対応

翔泳社・2025年6月・MySQL 8.4 LTS 対応

MySQL 8.4 に対応した入門書。日本MySQLユーザ会のメンバーが執筆している。

Amazonで見る

MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ

技術評論社・2024年5月

内部構造と動作原理から、運用・管理の考え方を学ぶ。

Amazonで見る

Amazonのアソシエイトとして、FIRE SE は適格販売により収入を得ています。