MySQLのEXPLAIN入門:実行計画の読み方と遅いSQLの調べ方
作成日:2026.10.05
MySQLのEXPLAINの使い方と、key・type・rows・Extraなどの読み方を、注文検索の例で解説します。複合インデックスの改善候補を比較する手順や、推定値と実測の違い、LIMIT付きの検索を検証するときの注意点もまとめます。
目次
MySQLでSQLの実行に時間がかかるとき、まず確認したいのは「どのようにデータを探しているか」です。インデックスがあるかだけでなく、どのインデックスを使い、どれくらいの行を調べる計画なのかを見ると、調査を進めやすくなります。
今回は、EXPLAINの基本操作と出力の読み方を整理します。Laravelのコードレビューで遅いSQLの候補を洗い出す記事では調査対象の見つけ方を扱いましたが、この記事では、取得したSQLの実行計画を読むところから進めます。
説明の対象はMySQL 8.0のInnoDBテーブルです。仕様は2026年10月5日時点のMySQL公式資料で確認しています。SQLと出力の数値は説明用の例で、同じ出力が得られることを示すものではありません。
EXPLAINは実行計画を確認するためのもの
実行計画は、MySQLがSQLを処理するために選んだ読み取り方や処理の組み合わせです。通常のEXPLAINでは、採用するインデックス、テーブルへのアクセス方法、読み取り行数の見積もりなどを確認できます。
ただし、通常のEXPLAINに表示される推定値から、元のSELECTが何秒で完了するかは分かりません。実行計画を見て改善の仮説を立て、元のSQLの計測で確かめる、という使い方になります。
基本操作は、調べたいSELECTの前にEXPLAINを付けるだけです。
EXPLAIN SELECT id FROM orders WHERE customer_id = 123;
表形式で読みたいときは、FORMAT=TRADITIONALを指定します。MySQL 8.0.32以降はexplain_formatの設定でも既定の表示形式が変わるため、以下の例では形式を明示します。使い方と対応する形式は、MySQL公式のEXPLAIN Statementで確認できます。
EXPLAIN FORMAT=TRADITIONAL
SELECT id FROM orders WHERE customer_id = 123;
なお、EXPLAIN orders;のようにテーブル名だけを指定すると、テーブルの列情報が表示されます。SQLの実行計画を調べるときは、SELECT文を続けて指定します。
注文検索を例にしてみる
注文情報を保存するordersテーブルを例にします。DDLは検証用DBでの作成例です。既存の業務テーブルへそのまま実行するものではありません。
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
customer_id BIGINT UNSIGNED NOT NULL,
status TINYINT UNSIGNED NOT NULL,
updated_at DATETIME NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
PRIMARY KEY (id),
INDEX idx_customer_id (customer_id),
INDEX idx_status (status)
) ENGINE=InnoDB;
顧客IDが123、状態が1で、指定日時より後に更新された注文を取得するSQLを考えます。
EXPLAIN FORMAT=TRADITIONAL
SELECT id, customer_id, total_amount
FROM orders
WHERE customer_id = 123
AND status = 1
AND updated_at > '2026-10-01 09:00:00';
単一テーブルの単純なSELECTなら、通常は実行計画が1行で表示されます。この1行は、検索結果の1件を表しているわけではありません。注文が何万件返る検索でも、そのテーブルをどう読むかが1行にまとまります。
アプリケーションから取り出したSQLにプレースホルダーの?がある場合は、バインド値も確認します。SQLクライアントで具体値を埋めて調べるなら、文字列や日時の引用符とエスケープに注意してください。値を変えると対象件数や計画も変わり得るため、実際の調査条件に合わせます。SQLや値を共有するときは、個人情報などのマスキングも必要です。
まずはkey・type・rowsを読む
最初から全列を覚えるより、「検索の入口」「読み取り方」「候補件数」を順に見ると理解しやすいかと思います。主要な列は次のとおりです。各列の定義は、MySQL公式のEXPLAIN Output Formatにまとまっています。
| 列 | 確認すること |
|---|---|
table | どのテーブルの計画か |
possible_keys | 行の検索に使える候補インデックス |
key | 実際に採用されたインデックス |
type | テーブルへのアクセス方法 |
rows | 調べる行数の見積もり |
filtered | テーブルの条件を通過する割合の見積もり |
Extra | 条件評価や並べ替えなどの補足 |
possible_keysとkeyは別の情報
注文検索で、次のような表示だったとします。説明用に一部の列を抜き出した架空の例です。
possible_keys: idx_customer_id, idx_status
key: idx_customer_id
この場合、候補は二つありますが、採用されたのはidx_customer_idです。顧客IDを入口に注文を探し、その候補に対して状態や更新日時の条件を評価する計画と考えられます。
possible_keysに名前が並んでいても、すべてを使っているわけではありません。単独インデックスを複数作る方法と、複数列を一つにまとめた複合インデックスも、同じ働きになるとは限りません。複数のインデックスを組み合わせるIndex Mergeという方法もありますが、採用された計画で確認します。
typeは速さの判定ではなくアクセス方法
| type | 読み取り方の目安 |
|---|---|
const | 主キー・一意キーの全列を固定値で指定するなど、最大1行を特定 |
ref | 非一意インデックスなどで、値が一致する行を検索 |
range | インデックスの一つ以上の範囲を検索 |
index_merge | 複数のインデックスの検索結果を組み合わせる |
index | インデックス全体を走査 |
ALL | テーブル全体を走査 |
refなら顧客IDの一致検索、rangeなら日時などの範囲検索、といった読み方ができます。特にindexは、インデックスを使っていても全体を走査する点に注意します。
ただし、refやrangeでも候補が数十万行なら処理量は多くなり得ます。逆に、小さなテーブルや大半の行を取得する検索では、ALLが合理的な場合もあります。名前だけで良い・悪いを決めず、対象件数と実測を合わせて見ます。
rowsとfilteredは推定値
次も実測ではなく、読み方を説明するための架空の出力です。
| type | key | rows | filtered | Extra |
|---|---|---|---|---|
ref | idx_customer_id | 200000 | 5.00 | Using where |
顧客インデックスで候補を探し、約20万行を調べると見積もっています。その候補のうち、追加の条件を通過する見込みが5%です。通過後の件数の目安は次の計算になります。
200,000 × 5.00 / 100 = 10,000行
filtered = 5は「5%が残る」という意味です。5%を除外する意味ではありません。また、rowsはテーブル総件数や実際の取得件数ではなく、InnoDBでは推定値です。この例でも、実際に1万行返ると確定したわけではありません。
多数の候補を読んでから大半を捨てる計画なら、最初から検索範囲を狭められないか検討できます。こうした読み方は、MySQL公式のCondition Filteringでも説明されています。
Extraは処理の補足として読む
Extraには追加の処理情報が表示されます。よく見かけるものを整理すると、次のようになります。
| 表示 | 意味と注意点 |
|---|---|
Using where | WHERE条件で行を絞る。表示されるだけで問題とはいえない |
Using index | 必要な列をインデックスだけで取得できる |
Using index condition | インデックス上で条件を評価し、通過した行の本体を読む |
Using filesort | 追加の並べ替えが必要。必ずディスクでソートする意味ではない |
Using temporary | 中間処理に一時テーブルを使う。必ずディスク上に作る意味ではない |
Using indexとUsing index conditionは名前が似ていますが、意味が違います。前者は必要な列をインデックスだけで取得する方法で、カバリングインデックスによる取得と呼ばれます。後者はIndex Condition Pushdown(ICP)で、インデックス上で条件を評価して、不要な行本体の読み取りを減らす方法です。詳細はMySQL公式のICPの説明を参照してください。
また、filesortはメモリ内で処理できる場合があり、内部一時テーブルもメモリ上とディスク上の両方があります。ORDER BY OptimizationとInternal Temporary Table Use in MySQLで、それぞれの挙動を確認できます。
これらの表示があれば、並べ替えや集計の対象件数と実測時間を調べます。処理が存在することと、それが遅延の主因であることは分けて考えます。
複合インデックスの候補を比較する
例の注文検索では、顧客IDと状態が等値条件、更新日時が範囲条件です。次の複合インデックスを改善候補として考えられます。追加は、同じ検索を再現できる検証用DBで行います。
CREATE INDEX idx_customer_status_updated
ON orders (customer_id, status, updated_at);
狙いは、顧客IDと状態の組み合わせで絞った範囲の中から、指定日時より後の注文を探すことです。顧客の全注文を候補にしてから状態と日時を評価するより、候補取得を減らせるかを確かめます。
複合インデックスでは列順に意味があります。基本的には先頭からの列の組み合わせを使った検索が重要です。今回なら、customer_id、customer_id・status、customer_id・status・updated_atという組み合わせを考えます。単独インデックスとの違いは、MySQL公式のMultiple-Column Indexesに説明があります。
範囲条件に使う列より後ろの列は、検索範囲をさらに狭めるためには使われない場合があります。ただし、ICPやカバリングによって役立つ場合もあるため、「範囲条件より後ろの列は無意味」とは決めつけません。範囲の作り方は、MySQL公式のRange Optimizationで確認できます。
追加前後で、同じSELECTに対してEXPLAINを実行し、次を比べます。
key:候補の複合インデックスが採用されたか。typeとrows:読み取り方と候補件数の見積もりがどう変わったか。key_len:検索にどのキー部分まで使っているか。Extra:条件評価や取得方法がどう変わったか。- 元のSELECT:結果が一致するか、同じ計測方法で処理時間が改善するか。
key_lenはバイト数で、単純な列数ではありません。列の型、文字コード、NULL可否などで変わるため、SHOW CREATE TABLEの定義と照合します。新しいインデックスが採用されることや、rowsが一定の数値まで減ることも保証されません。データの分布や統計情報によって計画は変わります。
インデックスを増やすと、書き込み時の維持コストと使用容量も増えます。この検索だけでなく、ほかのSQLやINSERT・UPDATE・DELETEへの影響も含めて判断します。MySQL公式のOptimization and Indexesでも、このコストとのバランスが説明されています。
LIMITは調べる行数の上限ではない
SQLクライアントで検索するときは、自動でLIMITが付いていないかも確認します。例えば、先ほどの検索にLIMIT 50000が付いている場合です。
SELECT id, customer_id, total_amount
FROM orders
WHERE customer_id = 123
AND status = 1
AND updated_at > '2026-10-01 09:00:00'
LIMIT 50000;
これは結果を最大5万件返す指定です。調べる行数を5万件に制限する指定ではありません。
このような単純な検索で、条件に一致する注文が十分にあれば、必要な5万件が見つかった時点で終了できます。一方、一致する注文が0件なら、検索対象の候補を調べて該当行がないことを確認する必要があります。適切なインデックスで範囲が狭ければすぐに終わることもありますが、大量の候補に追加条件を評価する計画なら、0件でも時間がかかり得ます。
また、ORDER BYや集計がある場合は、結果を返す前に追加処理が必要なことがあります。LIMITの有無で実行計画自体も変わり得るため、「LIMIT付きで速かったから元のSQLも速い」とは判断できません。詳細はMySQL公式のLIMIT Query Optimizationを参照してください。
同じ条件で実行計画と時間を記録する
改善候補を比べる前に、調べているDBと実際のテーブル定義を確認します。
SELECT VERSION();
SHOW CREATE TABLE orders;
SHOW INDEX FROM orders;
そのうえで、次の順に調べると比較しやすくなります。
- SQL全文、バインド値、選択する列、LIMITやORDER BY、データ件数と分布を記録する。
- 現行構成のEXPLAINを保存する。
- 安全な検証環境で元のSELECTを実行し、全結果の取得完了までを同じ方法で測る。
- 同じ開始データで改善候補を一つずつ比較し、EXPLAIN、結果、処理時間を記録する。
計測用にSELECT COUNT(*)へ置き換えると、取得する列や処理が変わり、計画も変わることがあります。件数を調べるためのSQLと、元のSELECTを測るためのSQLは区別します。並び順を指定しないSELECTでは、結果の一致を返却順だけで判断しないようにします。
比較の記録は、例えば次のような形にできます。「未記入」の欄には自分の環境で確認した値を記入します。
| ケース | 条件 | key | 推定rows | 結果件数 | 実測時間 |
|---|---|---|---|---|---|
| 追加前 | 通常条件・LIMITなし | 未記入 | 未記入 | 未記入 | 未記入 |
| 追加後 | 追加前と同じ条件 | 未記入 | 未記入 | 未記入 | 未記入 |
| 追加前 | 0件条件・LIMITなし | 未記入 | 未記入 | 未記入 | 未記入 |
| 追加後 | 追加前と同じ0件条件 | 未記入 | 未記入 | 未記入 | 未記入 |
SQLクライアントの表示や通信、アプリ側の処理が含まれる場合は、どこまでを測ったかも記録します。初回と再実行を分けて複数回測り、キャッシュや同時負荷の影響を考えます。タイムアウトした場合は「30秒でタイムアウト」などと残し、30秒で完了した値としては扱いません。
実データに近い件数だけでなく、特定の顧客に注文が集中しているといった分布も重要です。開発用の少量データだけで、本番相当の実行計画や時間を再現できるとは限りません。
詳しく調べたい場合はJSONやEXPLAIN ANALYZEも使う
表形式で読み取り方を把握した後、使われたキー部分などを詳しく確認したい場合は、JSON形式も使えます。
EXPLAIN FORMAT=JSON
SELECT id, customer_id, total_amount
FROM orders
WHERE customer_id = 123
AND status = 1
AND updated_at > '2026-10-01 09:00:00';
used_key_partsやattached_condition、index_conditionなどが、使用したキー部分や条件評価の手掛かりになります。出力される項目は計画によって異なります。JSON形式も通常のEXPLAINであり、cost_infoのコストを実行時間の秒数として読むものではありません。
MySQL 8.0.18以降には、実行時の時間・行数・繰り返し回数などを確認するEXPLAIN ANALYZEもあります。こちらは対象SQLを実際に実行します。SELECTでも大量のデータを調べれば負荷がかかるため、実行するSQLと環境を確認して利用してください。通常のEXPLAINと実測を組み合わせる方法は、EXPLAIN ANALYZEが使えない環境でも調査の基本になります。
JOINを含むSQLでは、結合元の行に応じて後続テーブルへの検索が繰り返されることがあります。表形式の各行のrowsを単純に足して総読み取り件数と考えず、結合順序と各段階の絞り込みを確認します。複雑な計画や実測との差を追う場合は、JSON・TREE形式や、利用できるスロークエリログなどで補足します。
まずは、keyで入口、typeで読み取り方、rowsとfilteredで候補の規模を見るところから始めればよいかと思います。その情報をもとに改善の仮説を立て、同じ条件の実測で確かめましょう。
参考資料
- MySQL 8.0 Reference Manual: EXPLAIN Statement
- MySQL 8.0 Reference Manual: EXPLAIN Output Format
- MySQL 8.0 Reference Manual: Condition Filtering
- MySQL 8.0 Reference Manual: Multiple-Column Indexes
- MySQL 8.0 Reference Manual: Range Optimization
- MySQL 8.0 Reference Manual: Index Condition Pushdown Optimization
- MySQL 8.0 Reference Manual: ORDER BY Optimization
- MySQL 8.0 Reference Manual: Internal Temporary Table Use in MySQL
- MySQL 8.0 Reference Manual: LIMIT Query Optimization
- MySQL 8.0 Reference Manual: Optimization and Indexes
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
Laravelのコードレビューで遅いSQLの候補を洗い出す
2026.10.02
Laravelのコードレビューで、ループ内のDBアクセスやEloquentのN+1、大量取得、検索条件とインデックスなど、性能問題につながりやすい箇所を候補として洗い出します。Laravelで発行SQLと実行時間を確認し、MySQLのEXPLAINや実データに近い環境での計測へつなげる方法を紹介します。コードレビューだけではSQLの実行時間を断定できない点も説明します。
XAMPPのMariaDBでERROR 1130が発生したときの復旧手順
2026.08.05
XAMPPのMariaDBでERROR 1130が発生し、phpMyAdminやCLIから接続できなくなった際の復旧手順を紹介します。接続先とプロセスを確認し、データディレクトリをバックアップした上で、復旧モードから破損したmysql.global_privを検査・修復し、通常起動後の接続確認まで行います。
XAMPPのMySQLが起動しなくなった時のデータ救出方法
2026.04.05
XAMPPのMySQL起動エラーでお困りの開発者向けに、データ損失を防ぎながら行なう応急的な復旧方法を詳しく紹介。通常の手段では解決しない場合、ファイルシステムレベルでのデータ救出技法を使う具体的な手順をまとめました。
MySQLのUPDATE文を使ったカラム値の一括置換
2025.02.06
MySQLで特定のカラム内の文字列を簡単に置換する方法を紹介します。この記事では、UPDATE文とREPLACE関数を組み合わせて、効率的にデータを更新する方法を解説します。