実行計画 — DB が何をしようとしているかを読む
推測でチューニングしない。EXPLAIN の読み方と、見積もりと実測のズレが何を意味するか。
先に読んでおくとよい記事
この記事の進み方
What — EXPLAIN が教えてくれること
実行計画は、DB が「このクエリをどう処理するつもりか」を宣言したものです。
- 1① 構文解析SQL を構文木に変換する軽い
- 2② 書き換えビューの展開、条件の整理軽い
- 3③ 最適化統計情報から、最も安い手順を選ぶ中
- 4④ 実行決めた手順どおりに処理する重い
ANALYZE / VACUUM ANALYZE で統計情報を更新すると、③の判断が変わる。インデックスがあるのに使われない場合、まず統計の鮮度を確認する。
実行計画は③の結果。オプティマイザは統計情報をもとにコストを見積もり、最も安いと判断した手順を選ぶ。統計が間違っていれば、選択も間違う。
何を見るか
そのテーブルをどう読むか。フルスキャンかインデックスか。
-- MySQL: type 列
ALL ← フルスキャン。大きなテーブルでは要注意
index ← インデックス全体のスキャン。ALL よりまし程度
range ← 範囲スキャン。良好
ref ← 非ユニークなインデックス検索。良好
eq_ref ← ユニークなインデックス検索。非常に良い
const ← 主キー等での1件確定。最速
-- PostgreSQL: ノード名
Seq Scan ← フルスキャン
Index Scan ← インデックス検索
Index Only Scan ← カバリング。テーブルを読まない- 見るべき危険信号
- ALL / Seq Scan
- 理想
- eq_ref / Index Only Scan
- 例外
- 小さいテーブルなら ALL でよい
- 見るべき点
- —
- 危険信号
- —
- 対処
- —
- 特にまずい
- —
- –ALL が常に悪いわけではない。数百行のマスタテーブルなら、インデックスを辿るより全件読むほうが速い。
- –問題は「大きなテーブルに ALL が出ている」場合。
全部を理解しようとせず、この3つから入る。ほとんどの問題はここに現れる。
- オプティマイザ
- どう実行するかを決める仕組み。統計情報をもとにコストを見積もり、最小のものを選ぶ。
- 統計情報
- テーブルの行数、値の分布、カーディナリティなど。実データそのものではなくサンプリングした要約。
- コスト
- オプティマイザが計算する相対的な処理量の見積もり。単位は秒ではなく、内部的な指標。
- Nested Loop Join
- 外側の各行に対し、内側を検索する結合方式。外側が少なく、内側にインデックスがあると速い。
- Hash Join
- 片方をハッシュテーブルにしてから結合する。大きなテーブル同士の結合に向く。
- Merge Join
- 両方をソートしてから順に突き合わせる。すでにソート済みなら効率がよい。
EXPLAIN が表示する行数は何ですか?
Why — なぜオプティマイザは間違えるのか
オプティマイザは推測しています。実際に実行してみるわけではありません。
推測の材料は統計情報です。そして統計情報には限界があります。
限界1:古くなる。 大量のデータを投入した直後、統計はまだ古い分布を指しています。冒頭の事故がこれです。
限界2:サンプリングである。 全行を調べるのはコストが高いので、一部を抽出して分布を推定します。偏ったデータでは実態と乖離します。
限界3:列どうしの相関を知らない。 prefecture = '東京都' AND city = '渋谷区' という条件で、オプティマイザは2つの条件を独立と仮定して選択率を掛け算します。しかし実際には渋谷区なら東京都に決まっているので、実際の絞り込みは想定より緩くなります。
限界4:値の偏りを表現しきれない。 status = 'paid' が85%、status = 'pending' が0.1% というような偏りは、ヒストグラムがないと「4種類だから25%ずつ」と推定されます。
結合順序が性能を決める
3つのテーブルを結合するとき、どの順で結合するかで性能が桁違いに変わります。
A(100万行) JOIN B(1000行) JOIN C(10行)
悪い順序: A → B → C
100万行を起点に、1行ずつ B を探す
良い順序: C → B → A
10行を起点に絞り込み、最後に A をインデックスで引く
オプティマイザはこの順序も決めています。行数の見積もりが間違っていると、この判断も間違います。 「1000行だと思って選んだ順序が、実際は100万行だった」という形で性能が崩れます。
クエリが遅いとき、最初にすべきことはどれですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この実行計画から、何が問題でどう直しますか?
- クエリ: SELECT o.*, u.name FROM orders o JOIN users u ON u.id = o.user_id WHERE o.status = 'pending' AND o.created_at >= '2026-09-01' ORDER BY o.created_at DESC LIMIT 50
- orders は 5000万行、users は 300万行
- EXPLAIN ANALYZE の結果(要約)
- orders: type=ALL, rows=49,800,000 (見積もり), actual rows=52,000, Extra=Using where; Using filesort
- users: type=eq_ref, key=PRIMARY, rows=1
- 既存のインデックス: orders(user_id), orders(created_at)
- status='pending' は全体の 0.1%(5万行)
- · 見積もり行数と実測行数のズレは何を示しているか
- · 既存の orders(created_at) はなぜ使われていないのか
- · filesort を消すには、インデックスに何を含める必要があるか
「コードを変えていないのにバッチが3倍遅くなった」という状況を、原因から説明してください
- インデックスは存在している
- 実行計画ではフルスキャンになっている
- 前日から大量のデータが投入された
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。