【SQL】EXPLAINで実行計画を読む基本

SQL

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

前回はインデックスがなぜ検索を速くするのかを解説しました。

インデックスを作成したら、それが本当にクエリで使われているかを確認する必要があります。

感覚や思い込みではなく、データベースに直接聞く方法がEXPLAINです。

この記事は上級者向けの内容として、EXPLAINの各項目の読み方と、実行計画からチューニングの糸口を見つける方法を扱います。

EXPLAINで何が分かるのか

クエリオプティマイザの実行計画を可視化する

SQL文を実行すると、MySQLのオプティマイザ(クエリの実行方法を決定するコンポーネント)は、テーブルのどこを・どういう順番で・どう読むかという実行計画を裏側で組み立てます。

EXPLAINは、この実行計画を人間が読める形で出力してくれるコマンドです。

EXPLAIN SELECT * FROM orders WHERE customer_id = 100;

SELECT文の先頭にEXPLAINを付けるだけで、実際にクエリを実行することなく、その実行計画だけを確認できます。

「このクエリはインデックスを使っているか」「何行スキャンする見込みか」といった、パフォーマンスチューニングに直結する情報が一覧で得られます。

なぜ実行計画を読む必要があるのか

インデックスを作成したのに、実際にはオプティマイザがそのインデックスを使わずフルテーブルスキャンをしている、というケースは珍しくありません。

WHERE句の書き方や、テーブルの統計情報、データの分布状況によって、オプティマイザの判断は変わります。

EXPLAINを使わずに「インデックスを作ったから速くなったはず」と思い込んでいると、実際には効いていないインデックスを放置してしまうことになります。

本番相当のデータ量で実行計画を確認する習慣を持つことが、パフォーマンス問題を未然に防ぐ最短ルートです。

基本の見方

以下のテーブルとインデックスを例に進めます。

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    status VARCHAR(20) NOT NULL,
    total_price INT NOT NULL,
    ordered_at DATETIME NOT NULL
);

CREATE INDEX idx_orders_customer_id ON orders(customer_id);

type列:アクセス方法の効率を見る

type列は、テーブルへのアクセス方法を表し、実行計画を読むうえで最初に確認すべき項目です。

EXPLAIN SELECT * FROM orders WHERE customer_id = 100;

代表的な値は、効率が良い順に次のようになります。

type意味
const主キーや一意インデックスで1行に絞り込める、最速のアクセス
refインデックスを使って複数行に絞り込む(今回の例はこれに該当)
rangeインデックスを使った範囲検索(BETWEEN、IN等)
indexインデックス自体を全件走査する
ALLインデックスを使わずテーブル全体を走査(フルテーブルスキャン)

typeALLになっている場合は、インデックスが効いていない、あるいはそもそも使われていないことを意味します。

まずはALLになっているクエリがないかを洗い出すのが、チューニングの出発点です。

key列:実際に使われたインデックス

key列には、オプティマイザが実際に選択したインデックス名が表示されます。

possible_keys列には「使える可能性があったインデックス」が並びますが、key列に何も表示されない場合、それらのインデックスは結局使われなかったことを意味します。

possible_keysに候補があるのにkeyNULLになっているクエリは、インデックスの設計かクエリの書き方のどちらかに見直しの余地があるサインです。

rows列とExtra列:見積もり行数と補足情報

rows列は、オプティマイザがそのステップで読み込むと見積もった行数の概算値です。

実際のテーブル件数に対してrowsが異常に大きい場合、絞り込みが効いていない可能性があります。

Extra列には、Using where(テーブル読み込み後にさらに絞り込みを行っている)やUsing filesort(インデックスを使わずソート処理が発生している)、Using temporary(一時テーブルを使っている)といった補足情報が表示されます。

特にUsing filesortUsing temporaryは、大量データで処理が重くなりやすい要注意サインとして覚えておく価値があります。

つまずきやすいポイント:インデックスがあるのに使われない

LIKE演算子の前方一致以外でインデックスが効かない

❌ Before:LIKE演算子を中間一致・後方一致で使い、インデックスが効かない

EXPLAIN SELECT * FROM customers WHERE email LIKE '%example.com';

email列にインデックスがあっても、LIKE '%example.com'のように検索文字列の先頭に%が付く中間一致・後方一致の場合、インデックスの並び順を利用できず、typeALLのままフルテーブルスキャンになります。

私が実際にこのパターンに遭遇したのは、メールアドレスのドメインで絞り込む検索機能を実装したときでした。

インデックスを作成したはずなのにEXPLAINで確認するとtype: ALLのままで、「なぜ効かないのか」を調べてこの仕様に行き着きました。

✅ After:前方一致に書き換えるか、全文検索の仕組みを検討する

-- 前方一致であればインデックスが効く
EXPLAIN SELECT * FROM customers WHERE email LIKE 'taro%';

ドメインでの絞り込みがどうしても必要な場合は、LIKE演算子を工夫するのではなく、ドメイン部分を別カラムに分離してインデックスを張る、あるいは全文検索用のインデックスを導入するといった設計変更を検討することになります。

「LIKE演算子はインデックスが効く場合と効かない場合がある」という事実を知っているだけで、設計段階での判断が変わってきます。

列にSQL関数をかけるとインデックスが効かない

❌ Before:WHERE句の列に関数をかけてしまい、インデックスが無効化される

EXPLAIN SELECT * FROM orders WHERE YEAR(ordered_at) = 2026;

ordered_at列にインデックスがあっても、YEAR(ordered_at)のように列そのものに関数をかけてしまうと、オプティマイザはインデックスの並び順をそのまま使えなくなり、typeALLになってしまいます。

✅ After:範囲条件に書き換えてインデックスを使えるようにする

-- 列に関数をかけず、範囲条件に書き換える
EXPLAIN SELECT * FROM orders WHERE ordered_at >= '2026-01-01' AND ordered_at < '2027-01-01';

「特定の年のデータを取得したい」という意図は同じでも、書き方を範囲条件に変えるだけでインデックスが正しく使われるようになります。

WHERE句の左辺の列を関数で加工しない、という原則を意識しておくと、こうした見落としを減らせます。

応用・一歩先の使い方

JOINを含むクエリの実行計画

複数テーブルを結合するクエリでは、EXPLAINの結果が複数行になり、結合の順番を確認できます。

EXPLAIN
SELECT c.name, o.total_price
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
WHERE c.prefecture = '東京都';

結果はid列の値が同じであれば上の行から順に処理され、先に読み込まれたテーブルの結果を使って次のテーブルを絞り込んでいく、という結合順序を表しています。

結合順序が想定と違う場合や、後段のテーブルのtypeALLになっている場合は、結合条件に使う列へのインデックスが不足している可能性が高いです。

EXPLAIN ANALYZEで実測値まで確認する

MySQL 8.0.18以降では、EXPLAIN ANALYZEを使うことで、見積もりだけでなく実際にクエリを実行した際の所要時間や実測の行数まで確認できます。

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100;

EXPLAINが「予想」を教えてくれるのに対し、EXPLAIN ANALYZEは実際にクエリを実行するため、見積もりと実測値のズレも確認できます。

ズレが大きい場合は、テーブルの統計情報が古くなっている可能性があり、ANALYZE TABLEで統計情報を更新することも選択肢に入ってきます。

ただし本番環境の重いクエリに対してEXPLAIN ANALYZEを実行すると、実際にクエリが走ってしまう点には注意が必要です。

まとめ

この記事のポイント

  • EXPLAINはSQL文の実行計画を、実際に実行せずに可視化できるコマンド
  • type列でアクセス方法の効率を、key列で実際に使われたインデックスを確認する
  • rowsExtra列(Using filesort・Using temporary等)から重い処理の兆候を読み取る
  • LIKE演算子の中間一致・後方一致や、列への関数適用はインデックスを無効化する典型パターン
  • EXPLAIN ANALYZEを使えば、見積もりだけでなく実測値まで確認できる

次に読むべき記事

実行計画の読み方が分かったら、次はトランザクションが複数同時に走ったときにデータの整合性がどう守られるのかを見ていきましょう。

次回は「ACID特性とは?トランザクションが守る4つの性質」を解説します。


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

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