MySQL EXPLAINの読み方:実行計画でSQLのボトルネックを特定する

スポンサーリンク

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  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

まず注目すべき列は typekeyrowsExtra の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 実際に使われたインデックス

keyNULL の場合、インデックスが使われていません。

-- 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 BYORDER 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 ALLindex が出たら要注意
key NULL ならインデックスが使われていない
rows 大きいほどスキャンコストが高い
Extra Using filesort / Using temporary は改善候補
  1. type=ALL + rows が大きい → インデックス追加を検討
  2. key=NULL → WHERE・JOIN条件のカラムにインデックスがあるか確認
  3. Using filesort → ORDER BY のカラムをインデックスに含める
  4. MySQL 8.0以降は EXPLAIN ANALYZE で実際の実行時間も確認できる