MySQLの性能問題を調査するとき、まず使うのが
EXPLAINコマンドです。ところがEXPLAINの出力はフィールドが多く、とくにtype・rows・Extraの3つをどう読めばよいか迷うケースが多いです。この記事では、MySQLのEXPLAIN出力を読む方法を解説します。各フィールドの意味と確認ポイント、type別のスキャン方式の比較、Extraフィールドで分かる性能問題のパターン、EXPLAIN結果をもとにインデックスを追加する実践手順、そしてMySQL 8.0で使えるEXPLAIN ANALYZEまで、RHEL 9.4 / Ubuntu 24.04 LTSで動作確認した手順をお伝えします。
この記事のポイント
・type列が「ALL」ならフルテーブルスキャン——まず疑う
・possible_keysにあってkeyがNULLなら候補があっても使われていない
・Extraに「Using filesort」「Using temporary」が出たら要注意
・EXPLAIN ANALYZEで推定コストと実行コストの乖離を実測できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜMySQLのEXPLAIN出力を読む必要があるのか
MySQLが遅い場合、まずスロークエリログで重いSQLを特定し、次にそのSQLにEXPLAINを付けて実行計画を確認します。EXPLAINはMySQLのオプティマイザーが「どのようにデータを取得するか」を出力するコマンドです。実際にクエリを実行せずに実行計画だけを取得できるため、本番環境でも安全に使えます。
EXPLAINを使わずにSQLを最適化しようとすると、インデックスを追加してもどのインデックスが効いているか分からず、試行錯誤を繰り返すことになります。逆にEXPLAINを読めるようになると、インデックスが使われているかどうかを数秒で判断できます。
現場でよく聞かれるのが「インデックスを張ったのに速くならない」という相談です。こういったケースのほとんどは、EXPLAINを確認するとすぐに原因が分かります。インデックスが使われていない理由(型の不一致・関数適用・最左一致違反など)がEXPLAINに明示されているからです。
EXPLAINコマンドの基本的な使い方
1. SELECT文の前にEXPLAINを付けるだけ
基本的な使い方はシンプルです。調べたいSELECT文の先頭にEXPLAINを付けます。
-- 基本形(テーブル全体の実行計画を確認する) EXPLAIN SELECT * FROM orders WHERE customer_id = 100; -- JOINを含む場合も同様に付けるだけでよい EXPLAIN SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.created_at >= '2026-01-01';
実行すると以下のような出力が得られます(RHEL 9.4上のMySQL 8.0系での実測例)。
mysql> EXPLAIN SELECT * FROM orders WHERE customer_id = 100\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: orders partitions: NULL type: ref possible_keys: idx_cust_id key: idx_cust_id key_len: 4 ref: const rows: 23 filtered: 100.00 Extra: NULL 1 row in set, 1 warning (0.00 sec)
2. 出力フィールドの全体像
主要なフィールドとその確認優先度を以下に示します。
| フィールド名 | 内容 | 優先度 |
|---|---|---|
| type | テーブルアクセス方式(ALL/index/range/ref/eq_ref/const) | 最重要 |
| key | 実際に使われたインデックス名(NULLなら未使用) | 最重要 |
| possible_keys | 使用可能なインデックスの候補一覧 | 高 |
| rows | オプティマイザーが検索すると推定する行数 | 高 |
| filtered | WHERE条件で絞り込まれる割合(%)の推定値 | 中 |
| Extra | 追加情報(Using filesort / Using temporary 等) | 高 |
typeフィールドで把握するスキャン方式の比較
typeフィールドはEXPLAIN出力の中で最も重要です。どのようにテーブルを検索するかを示しており、以下の順で効率が高くなります。
ALL → index → range → ref → eq_ref → const(左ほど非効率、右ほど効率的)
ALLはフルテーブルスキャン——まず疑う
typeがALLの場合、テーブル全行を順番に読み込みます。テーブルが大きいほど遅くなり、数百万行のテーブルに対してALLが出るとクエリが数秒以上かかることも珍しくありません。
-- インデックスがない場合や後方一致LIKE検索でALLが出る例 EXPLAIN SELECT * FROM orders WHERE memo LIKE '%urgent%'; -- → type: ALL(後方一致はBツリーインデックスを使えない) EXPLAIN SELECT * FROM orders WHERE customer_id = 100; -- customer_idにインデックスがなければ → type: ALL
ALLが出ても、件数が数百行程度の小さいテーブルであれば実務上の問題にはなりにくいです。問題になるのは大量データのテーブルでALLが出る場合です。
index・range・ref・eq_refの違い
・index: インデックス全体を順番に読む(フルインデックススキャン)。ALLよりはディスクI/Oが減るが非効率な場合がある
・range: インデックスの範囲スキャン。BETWEEN・>=・<=・INなどWHERE節に範囲条件がある
・ref: 非ユニークインデックスを等値条件(=)で検索する。複数行がヒットする可能性がある
・eq_ref: プライマリキーまたはユニークインデックスを等値条件で検索する。JOINのON句で1行だけヒットする場合に出る
constとsystemは理想形
constは、プライマリキーまたはユニークインデックスの全カラムを等値条件で指定した場合に出ます。1行だけヒットすることがオプティマイザーに分かっており、定数として扱われるためコストがほぼゼロです。
EXPLAIN SELECT * FROM customers WHERE id = 1; -- idがPRIMARY KEY → type: const EXPLAIN SELECT id FROM customers WHERE id = 1; -- → type: const, Extra: Using index(カバリングインデックス)
rowsとkeyフィールドで絞り込み効率を確認する
1. possible_keysとkeyの違い
possible_keysは「使える可能性があるインデックス」の候補リストです。一方、keyは「実際にオプティマイザーが選択したインデックス」です。
注目すべきパターンは2つです。
・possible_keysに候補があるのにkeyがNULL: インデックスが存在するが使われていない。クエリの書き方(関数適用・型不一致など)が原因のことが多い
・possible_keysもkeyもNULL: 対象カラムにインデックスが存在しない。インデックス作成を検討する
2. rowsはオプティマイザーの推定行数
rowsはオプティマイザーがスキャンすると推定する行数です。実際の行数ではなく推定値ですが、インデックスが効いているかどうかの目安になります。
例えば、100万行のテーブルに対してrowsが100万と出ていればほぼフルスキャンです。rowsが数十~数百であれば効率的なインデックス検索ができています。
3. filteredも一緒に確認する
filteredはrowsの中でWHERE条件に一致すると推定される割合(%)です。rowsが1,000でfilteredが10.00なら、WHERE条件を通過するのは100行と推定されます。filteredが低ければ、インデックスで絞り込みきれずにWHERE条件で多くの行を読み捨てていることを意味します。複合インデックスを検討するサインです。
Extraフィールドで見つかる典型的な性能問題
Extraフィールドはオプティマイザーの追加情報を示します。性能問題の手がかりとなるキーワードを確認します。
Using filesort——ソートにインデックスが使われていない
Using filesortが出る場合、ORDER BYのソートにインデックスが使われず、ファイルソート(メモリ上またはディスク上でのソート)が発生しています。大量データのソートでは大きな遅延の原因になります。
EXPLAIN SELECT * FROM orders ORDER BY created_at DESC LIMIT 100; -- Extra: Using filesort → created_atにインデックスを追加する -- 複合インデックスでWHERE + ORDER BYを同時にカバーする例 CREATE INDEX idx_cust_created ON orders (customer_id, created_at);
Using temporary——一時テーブル生成で重くなる
Using temporaryはGROUP BYやDISTINCTの処理で一時テーブルが作成されることを意味します。大量データのGROUP BYは特に重く、複合インデックスの見直しで解消できることがあります。Using filesortとUsing temporaryが同時に出る場合は、クエリ全体の見直しが必要なことが多いです。
Using index(カバリングインデックス)は歓迎
Using indexはテーブル本体を読まずにインデックスだけで必要な値が取得できた状態(カバリングインデックス)を示します。ディスクI/Oが大幅に削減されるため、性能面で理想的な状態です。
Using whereとUsing index conditionの違い
・Using where: インデックスで絞り込んだ後、テーブルを読んでWHERE条件をさらに評価している
・Using index condition(ICP): インデックスレベルでWHERE条件を評価してからテーブルにアクセスする。MySQL 5.6以降の最適化で、Using whereよりテーブルアクセスを減らせる
EXPLAIN結果をもとにインデックスを追加する実践手順
1. どのカラムにインデックスを張るか判断する
インデックスを張るカラムの判断基準は以下のとおりです。
・WHERE条件で頻繁に使われる等値条件・範囲条件のカラム
・ORDER BYやGROUP BYで使われるカラム(ソートの高速化)
・JOINのON句で使われるカラム(外部キーなど)
一方、更新・挿入が多いテーブルにインデックスを増やしすぎると、更新コストが増加します。1テーブルにインデックスを5本以上張る場合は必要性を精査してください。
2. CREATE INDEXでインデックスを追加する
-- 単独インデックスを追加する CREATE INDEX idx_created_at ON orders (created_at); -- 複合インデックス(WHERE条件 + ORDER BY を一緒にカバーする) CREATE INDEX idx_cust_created ON orders (customer_id, created_at); -- インデックス一覧を確認する SHOW INDEX FROM orders; -- 不要なインデックスを削除する場合 DROP INDEX idx_old_idx ON orders;
3. インデックス追加後にEXPLAINで効果確認
インデックスを追加したら、必ず同じEXPLAINを再実行して確認します。期待どおりにkey列にインデックス名が表示されること、typeがALLから改善されていることをチェックします。
-- インデックス追加後に再確認する EXPLAIN SELECT * FROM orders WHERE customer_id = 100 ORDER BY created_at DESC\G -- 改善後の理想形: type: ref, key: idx_cust_created, Extra: Using index condition
EXPLAIN ANALYZEで実際の実行時間まで計測する(MySQL 8.0以降)
通常のEXPLAINはオプティマイザーの「推定」です。MySQL 8.0以降ではEXPLAIN ANALYZEを使うことで、実際にクエリを実行した際のコストと推定コストの乖離を確認できます。
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100\G
実行すると以下のような出力が得られます。
-> Index lookup on orders using idx_cust_id (customer_id=100) (cost=4.51 rows=23) (actual time=0.045..0.138 rows=21 loops=1)
括弧の読み方は以下のとおりです。
・cost=4.51 rows=23: オプティマイザーの推定コストと推定行数
・actual time=0.045..0.138: 最初の行を返すまでの時間と全行を返すまでの時間(ミリ秒)
・rows=21: 実際にヒットした行数
推定rows(23)と実際rows(21)が近ければ統計情報が正確な状態です。乖離が大きい場合はANALYZE TABLE テーブル名;で統計情報を更新してみてください。
注意点として、EXPLAIN ANALYZEは実際にクエリを実行するため、UPDATE/DELETEに適用すると本番データが変更されます。本番環境ではSELECT文のみに限定して使ってください。
トラブルシュート——よくある「EXPLAINが改善しない」パターン
インデックスを追加したのにEXPLAINのtypeが改善しない場合の確認ポイントを以下にまとめます。
| 症状 | 確認ポイントと対処 |
|---|---|
| keyがNULLのままで変わらない | インデックスが作成されているかSHOW INDEX FROM テーブル名;で確認。統計情報が古い場合はANALYZE TABLE テーブル名;を実行する |
| インデックスがあるのにtypeがALL | WHERE条件の型とインデックスカラムの型が一致しているか確認(VARCHARカラムに数値で検索するとインデックスが使われない) |
| LIKE検索でインデックスが使われない | 後方一致('%keyword')・中間一致('%keyword%')はBツリーインデックスが使えない。全文検索インデックス(FULLTEXT)への切り替えを検討する |
| インデックスカラムに関数を使っている | WHERE YEAR(created_at) = 2026のように関数をかけるとインデックスが使われない。WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01'の範囲条件に書き直す |
| 複合インデックスの順序が違う | 複合インデックス(a, b)はWHERE a = ?やWHERE a = ? AND b = ?には効くが、WHERE b = ?単独には効かない(最左一致の原則) |
本記事のまとめ
MySQLのEXPLAIN出力の読み方を解説しました。要点を以下にまとめます。
| 確認項目 | チェック方法と対処 |
|---|---|
| type が ALL | フルテーブルスキャン——WHERE条件カラムにインデックスを追加する |
| key が NULL | インデックスが使われていない——SHOW INDEXで存在確認、ANALYZE TABLEで統計更新 |
| rows が大きい | 多数行を読んでいる——インデックスの絞り込みを改善する |
| Extra に Using filesort | ORDER BYにインデックスが使えていない——ORDER BYカラムを含む複合インデックスを検討する |
| Extra に Using temporary | GROUP BY/DISTINCTで一時テーブルが発生——クエリ・インデックス設計を見直す |
| Extra に Using index | カバリングインデックスで理想的な状態——そのまま維持する |
| 推定rowsと実際rowsの乖離 | 統計情報が古い——ANALYZE TABLE テーブル名; で更新する |
MySQLのチューニングを体系的に身につけるには、Linux基盤の「型」が必要です
EXPLAINでインデックスを最適化するには、MySQLのストレージエンジンの仕組みやLinuxサーバー上でのプロセス管理・ディスクI/Oの基礎を理解することが重要です。独学で断片的に覚えるより、現場で実際に使われる設計パターンを一度体系的に身につけることで、根拠を持って性能チューニングができるようになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます