MySQL EXPLAINの読み方:実行計画でSQLのボトルネックを特定する
EXPLAINとは
EXPLAIN はMySQLがSQLをどのように実行するかを表示するコマンドです。インデックスが使われているか、何行スキャンしているかを確認でき、スローなクエリの原因を特定するときに使います。
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
出力の見方
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | 1 | SIMPLE | users | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
まず注目すべき列は type・key・rows・Extra の4つです。
type:アクセス方法(最重要)
パフォーマンスに直結する最重要列です。上ほど速く、下ほど遅い。
| type | 意味 | 評価 |
|---|---|---|
const |
主キー or ユニークキーで1行に確定 | 最速 |
eq_ref |
JOINで主キー or ユニークキーを使用 | 速い |
ref |
インデックスで複数行にマッチ | 良い |
range |
インデックスの範囲スキャン(BETWEEN・>・<など) | 許容範囲 |
index |
インデックスを全件スキャン | 遅い |
ALL |
テーブルフルスキャン | 最遅・要改善 |
ALL が出たら要注意。 大きなテーブルで ALL が出ている場合はインデックスの追加を検討します。
-- typeがALLになる例(インデックスなし) EXPLAIN SELECT * FROM users WHERE name = '田中'; -- インデックス追加後はrefになる CREATE INDEX idx_name ON users(name);
key:実際に使われたインデックス
| 列 | 意味 |
|---|---|
possible_keys |
使える可能性があるインデックス |
key |
実際に使われたインデックス |
key が NULL の場合、インデックスが使われていません。
-- keyがNULLの場合 → フルスキャン +------+-----------+------+ | type | key | rows | +------+-----------+------+ | ALL | NULL | 9876 | +------+-----------+------+ -- インデックスが使われている場合 +------+-----------+------+ | type | key | rows | +------+-----------+------+ | ref | idx_email | 1 | +------+-----------+------+
rows:推定スキャン行数
MySQLが統計情報をもとに推定するスキャン行数です。実際の件数とは異なる場合があります。
- 少ないほど良い
- JOINがある場合は各テーブルの
rowsの積が実際の処理量に近い type=ALLかつrowsが大きい場合は特にパフォーマンスへの影響が大きい
-- JOINの例 +----+-------+------+------+ | id | table | type | rows | +----+-------+------+------+ | 1 | orders| ALL | 5000 | ← 5000行スキャン | 1 | users | ref | 1 | ← ordersの各行に対して1行 +----+-------+------+------+ -- 実質5000 × 1 = 5000行の処理
Extra:追加情報(要注意フラグあり)
| Extra | 意味 | 対処 |
|---|---|---|
Using index |
インデックスのみで完結(テーブルアクセス不要) | 良い状態 |
Using where |
WHERE条件でフィルタリング | 通常 |
Using filesort |
ORDER BYをインデックスで処理できずソートが発生 | 要確認 |
Using temporary |
GROUP BY・ORDER BYで一時テーブルが作成された | 要改善 |
Using index condition |
インデックスで条件を評価(Index Condition Pushdown) | 良い |
Using filesort
ORDER BY にインデックスが使われず、ソート処理が走っている状態です。
-- filesortが発生する例 EXPLAIN SELECT * FROM users ORDER BY created_at DESC; -- created_atにインデックスを追加すると解消できる CREATE INDEX idx_created_at ON users(created_at);
Using temporary
GROUP BY や ORDER BY の処理で一時テーブルが作られています。大量データで発生するとメモリ・I/Oに影響します。
-- Using temporaryが出やすいケース EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY status;
EXPLAIN ANALYZE(MySQL 8.0以降)
EXPLAIN ANALYZE を使うと推定値だけでなく実際の実行時間と行数が取得できます。
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
-> Index lookup on users using idx_email (email='test@example.com') (cost=0.35 rows=1) (actual time=0.032..0.034 rows=1 loops=1)
rows=1(推定)と actual ... rows=1(実際)を比較することで統計情報のズレも確認できます。
実践的な確認手順
-- 1. スロークエリを特定 EXPLAIN SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'pending' ORDER BY o.created_at DESC; -- 2. 確認するポイント -- ① typeにALLがないか -- ② keyがNULLになっていないか -- ③ rowsが想定外に大きくないか -- ④ ExtraにUsing filesort / Using temporaryがないか -- 3. 問題があればインデックスを追加して再確認 CREATE INDEX idx_status_created ON orders(status, created_at); EXPLAIN SELECT ...; -- 再度確認
まとめ
| 確認列 | 見るポイント |
|---|---|
type |
ALL や index が出たら要注意 |
key |
NULL ならインデックスが使われていない |
rows |
大きいほどスキャンコストが高い |
Extra |
Using filesort / Using temporary は改善候補 |
type=ALL+rowsが大きい → インデックス追加を検討key=NULL→ WHERE・JOIN条件のカラムにインデックスがあるか確認Using filesort→ ORDER BY のカラムをインデックスに含める- MySQL 8.0以降は
EXPLAIN ANALYZEで実際の実行時間も確認できる