クエリが遅い原因を調べるとき、まず使うべきコマンドが EXPLAIN です。SELECT 文の先頭に EXPLAIN を付けるだけで、MySQLがそのクエリをどう実行するか——どのインデックスを使うか、何行スキャンするか——を事前に確認できます。出力の中でも特に重要なのが type(アクセス方式)、key(使用インデックス)、rows(推定スキャン行数)、Extra(付加情報)の4列です。この記事では、それぞれの読み方と改善のヒントを実例つきで解説します。
このシナリオで考える
ECサイトの注文管理を想定した orders テーブルを使って説明します。
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
status ENUM('pending','paid','shipped','cancelled') NOT NULL,
total DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_user_id (user_id),
INDEX idx_status (status)
);
10万件ほどのレコードが入っており、user_id と status にはそれぞれ単独インデックスが張ってあります。以降のSQLはすべてこのテーブルをもとにしています。
EXPLAINの基本構文と主要カラム
使い方はシンプルです。
EXPLAIN SELECT * FROM orders WHERE user_id = 42;
出力には多くの列が並びますが、まず押さえるべき6つを表にまとめます。
| カラム名 | 意味 |
|---|---|
type |
テーブルへのアクセス方式。効率を示す最重要指標 |
possible_keys |
オプティマイザが候補として考慮したインデックス一覧 |
key |
実際に選ばれたインデックス(NULL はインデックス未使用) |
key_len |
使用インデックスのバイト数(複合インデックスで何列使ったかの手掛かり) |
rows |
スキャンが必要と推定される行数。大きいほど重い |
Extra |
ソートや一時テーブルの有無など、処理の詳細 |
MySQL 8.0 では EXPLAIN FORMAT=JSON や、実際に実行して実測値も取得できる EXPLAIN ANALYZE も使えます。まずはデフォルト出力の読み方を習得しておくと、どの形式でも応用できます。
type列で「アクセス効率」を判断する
type 列はクエリ最適化の出発点です。値は以下の順で効率が上がります。悪い方から順に並べました。
| type値 | 内容 | 評価 |
|---|---|---|
ALL |
フルテーブルスキャン。全行を読む | 要改善 |
index |
インデックス全体をスキャン。ALLより少し速い程度 | 要改善 |
range |
インデックスの範囲スキャン(BETWEEN、>、LIKE 'abc%' 等) |
許容範囲 |
ref |
非ユニークインデックスへの等値検索 | 良好 |
eq_ref |
ユニークインデックスへの等値検索(JOINの結合キーなど) | 良好 |
const |
主キーまたはユニークキーで1行に確定 | 最良 |
user_id = 42 の検索は idx_user_id が有効なら type = ref になります。インデックスがない列で検索すると type = ALL となり、10万行を全件スキャンします。
-- インデックスあり → type: ref
EXPLAIN SELECT * FROM orders WHERE user_id = 42;
-- key: idx_user_id, rows: 数十行(推定)
-- インデックスなしの列 → type: ALL
EXPLAIN SELECT * FROM orders WHERE total > 10000;
-- key: NULL, rows: 100000(全件スキャン)
type = ALL を見かけたら、WHERE 句の条件列にインデックスが必要かどうかを最初に確認しましょう。
keyとrowsでインデックス活用度を確認する
key が NULL の場合、インデックスが一切使われていません。possible_keys に候補が載っているのに key が NULL の場合は、統計情報の偏りやオプティマイザの判断でフルスキャンの方が速いとみなされています。
rows は推定値ですが、全体行数の 50% 以上が出ている場合は警戒が必要です。インデックスを使っても実質フルスキャンに近く、オプティマイザがインデックスを使わない判断をすることもあります。
-- cancelled は全体の 1% 程度 → rows が小さく効率的
EXPLAIN SELECT * FROM orders WHERE status = 'cancelled';
-- paid が全体の 70% の場合 → rows が多く、インデックスを使わない判断になることも
EXPLAIN SELECT * FROM orders WHERE status = 'paid';
filtered 列も参考になります。「rows で推定された行数のうち、WHERE 条件を通過する割合(%)」を表します。値が低いほど後で多くの行が捨てられる、つまり無駄なスキャンが多いサインです。
Extra列でオプティマイザの内部処理を確認する
Extra 列には処理の内訳を表す文字列が入ります。代表的な値を確認しておきましょう。
| Extra値 | 意味 | 評価 |
|---|---|---|
Using index |
インデックスだけで結果を返せた(カバリングインデックス) | 最良 |
Using where |
ストレージエンジン側でWHERE絞り込みを実施 | 通常 |
Using filesort |
インデックスを使えないソートが発生。大量データではディスクを使う場合も | 要改善 |
Using temporary |
一時テーブルを作成(GROUP BY や DISTINCT の処理など) | 要改善 |
Using index condition |
ICP(Index Condition Pushdown)でストレージ層で絞り込み | 良好 |
Using filesort と Using temporary が同時に出ている場合は特に注意が必要です。GROUP BY や ORDER BY の対象列にインデックスを追加することで解消できるケースが多くあります。
-- Using filesort が出る例(user_id で絞り、created_at でソート)
EXPLAIN SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC;
-- (user_id, created_at) の複合インデックスを追加すると解消できる
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);
-- 追加後は Using filesort が消える
EXPLAIN SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC;
複合インデックスで ORDER BY を吸収できるのは、WHERE 条件列と ORDER BY 列を組み合わせた場合です。順序が逆(created_at, user_id)では効果がありません。
よくある落とし穴と注意点
- rows はあくまで推定値:実際の行数と乖離が大きい場合は
ANALYZE TABLE orders;で統計情報を更新します。 - 列を関数で囲むとインデックスが効かない:
WHERE DATE(created_at) = '2026-05-22'はtype = ALLになります。WHERE created_at >= '2026-05-22 00:00:00' AND created_at < '2026-05-23 00:00:00'と書き直しましょう。 - LIKE の前方一致以外はインデックス不使用:
LIKE '%keyword'やLIKE '%keyword%'ではインデックスが使えません。前方一致LIKE 'keyword%'のみ有効です。 - 型の不一致に注意:VARCHAR 列を整数で検索する(
WHERE code = 42のcodeが VARCHAR)と暗黙の型変換が発生し、インデックスが無効化されます。クォートで文字列として渡しましょう。
まとめ
EXPLAIN の読み方は、type(全件スキャンかインデックス活用か)→ key(どのインデックスが使われたか)→ rows(推定スキャン行数)→ Extra(ソートや一時テーブルの有無)の順に確認するのが定石です。type = ALL や Using filesort を見つけたら、インデックスの追加や複合インデックスの見直しで改善できることがほとんどです。まずは手元のスロークエリに EXPLAIN をかけて読む習慣をつけると、チューニングの感覚が身につきます。
参考リンク
アイキャッチ画像: Photo by Jantine Doornbos on Unsplash
