【MySQL】EXPLAINでMySQLの実行計画を読む

MySQL

こんにちは、かつコーチです。

前回はインデックスの種類を整理しましたが、「インデックスを張ったのに速くならない」という相談は実務でも頻繁に起こります。

原因を切り分けるには、MySQLが実際にどうクエリを実行しているかを可視化するEXPLAINが欠かせません。

今回は上級者向けの内容として、EXPLAINの各カラムの読み方と、遅いクエリを見抜くポイントを解説します。

EXPLAINの基本的な使い方

クエリの先頭にEXPLAINを付けるだけ

EXPLAINは、対象のSELECT文の先頭に付けるだけで実行計画(オプティマイザがどう実行しようとしているか)を出力してくれます。

EXPLAIN
SELECT o.id, o.status, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 20;

MySQL 8.0ではEXPLAIN FORMAT=TREEEXPLAIN ANALYZEも使え、EXPLAIN ANALYZEは実際にクエリを実行した上で、各ステップの実測コストと実行時間まで出してくれます。

EXPLAIN ANALYZE
SELECT o.id, o.status, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 20;

見積もりだけのEXPLAINと、実測を伴うEXPLAIN ANALYZEを使い分けることで、オプティマイザの想定と現実のズレも把握できます。

各カラムの読み方

type:アクセス方法を示す最重要カラム

typeは、そのテーブルへのアクセス方法を示す最も重要なカラムで、良い順に並べると次のようになります。

type意味
const / system主キー・ユニークキーで1行に確定
eq_refJOINで相手側が主キー・ユニークキー1行に確定
ref非ユニークインデックスでの等価検索
rangeインデックスを使った範囲検索
indexインデックスのフルスキャン
ALLフルテーブルスキャン

type: ALLが出ている場合はインデックスがまったく使われておらず、テーブルの全行を読んでいる状態なので、まず疑うべきポイントです。

key・possible_keys:使われたインデックス

possible_keysはオプティマイザが候補として検討したインデックス、keyは実際に採用されたインデックスを示します。

possible_keysに候補があるのにkeyNULLになっている場合、インデックスがあっても使われなかったことを意味し、原因調査が必要なサインです。

rows・filtered:見積もり行数と絞り込み率

rowsはそのステップで読み込むと見積もられた行数、filteredはWHERE条件でさらに絞り込まれる割合(%)です。

rowsが実際のテーブル行数に近い、あるいはfilteredの値が極端に低い場合は、インデックスの効きが悪く、多くの行を無駄に読んでいる可能性が高いです。

Extra:見落としがちな要注意項目

Extraカラムには補足情報が入り、次のような表示には注意が必要です。

-- ソート対象がインデックス順で取れず、追加のソート処理が発生している例
EXPLAIN
SELECT * FROM orders WHERE customer_id = 1001 ORDER BY total_amount DESC;
  • Using filesort:インデックス順で取得できず、別途ソート処理が発生している
  • Using temporary:一時テーブルを作って処理している(GROUP BYやDISTINCTで発生しやすい)
  • Using index:インデックスだけでデータが取れている(カバリングインデックス、テーブル本体へのアクセスが不要で高速)

Using filesortUsing temporaryは特にパフォーマンスへの影響が大きく、この2つが出ていないかは毎回チェックする習慣をつけておくべきです。

実行計画から改善につなげる

インデックスが使われないケースの典型例

keyNULLになりやすい典型例をいくつか挙げます。

-- ❌ カラムに関数をかけると、そのカラムのインデックスは使われない
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2026;

-- ✅ 範囲検索に書き換えることでインデックスが使われる
EXPLAIN SELECT * FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-12-31 23:59:59';
-- ❌ カラムの型と異なる型で比較すると暗黙の型変換が起き、インデックスが使われないことがある
EXPLAIN SELECT * FROM orders WHERE order_code = 12345; -- order_codeはVARCHAR

-- ✅ カラムの型に合わせて比較する
EXPLAIN SELECT * FROM orders WHERE order_code = '12345';

インデックス対象のカラムに関数や演算、暗黙の型変換をかけてしまうと、インデックスの木構造をそのまま辿れなくなるため、オプティマイザがフルスキャンを選んでしまいます。

私が実行計画を読み違えた失敗談

以前、ORDER BYにLIMITを組み合わせたクエリが遅いという相談を受け、EXPLAINtyperefになっていたので「インデックスは使えているから原因は別だろう」と判断してしまったことがあります。

実際はExtraUsing filesortが出ており、WHERE句の絞り込みにはrefでインデックスが効いていたものの、ORDER BYのソート自体は別処理で行われていたのが遅さの正体でした。

typeだけを見て「インデックスが効いているから問題ない」と判断せず、Extraまで含めて総合的に読む必要があると学んだ失敗でした。

最終的にはORDER BYのカラムを含む複合インデックスに張り替えることでUsing filesortが消え、レスポンスが体感できるレベルで改善しました。

まとめ

この記事のポイント

  • EXPLAINはSELECT文の先頭に付けるだけで実行計画を確認でき、EXPLAIN ANALYZEは実測値まで見られる
  • typeはアクセス方法を示す最重要カラムで、ALLはフルテーブルスキャンのサイン
  • possible_keysにあるのにkeyNULLはインデックス不使用のサイン
  • ExtraUsing filesortUsing temporaryは特に注意して確認する
  • typeだけでなくExtraまで含めて総合的に読まないと、原因を見誤ることがある

次に読むべき記事

次回は「スロークエリログの見方と改善の第一歩」です。

EXPLAINで個別クエリを分析する前段階として、そもそも「どのクエリが遅いのか」を見つける方法を解説します。

タグ: MySQL, 上級者向け, パフォーマンス

タイトルとURLをコピーしました