仕組みから学ぶ Web
発展読了目安 15#EXPLAIN#実行計画#オプティマイザ#統計情報

実行計画 — DB が何をしようとしているかを読む

推測でチューニングしない。EXPLAIN の読み方と、見積もりと実測のズレが何を意味するか。

この記事の進み方

What — EXPLAIN が教えてくれること

実行計画は、DB が「このクエリをどう処理するつもりか」を宣言したものです。

Figureクエリが実行されるまで
  1. 1① 構文解析SQL を構文木に変換する軽い
  2. 2② 書き換えビューの展開、条件の整理軽い
  3. 3③ 最適化統計情報から、最も安い手順を選ぶ
  4. 4④ 実行決めた手順どおりに処理する重い

ANALYZE / VACUUM ANALYZE で統計情報を更新すると、③の判断が変わる。インデックスがあるのに使われない場合、まず統計の鮮度を確認する。

1/4
① 構文解析文法エラーやテーブルの存在を確認する。ここは高速で、性能に影響しない。

実行計画は③の結果。オプティマイザは統計情報をもとにコストを見積もり、最も安いと判断した手順を選ぶ。統計が間違っていれば、選択も間違う。

何を見るか

CompareEXPLAIN で最初に見る3か所

そのテーブルをどう読むか。フルスキャンかインデックスか。

-- 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 が表示する行数は何ですか?

まず選ぶ(解答例は a〜d の記号で説明します)

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万行だった」という形で性能が崩れます。

確認 — ここまで読めたか

クエリが遅いとき、最初にすべきことはどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

この実行計画から、何が問題でどう直しますか?

与えられた条件
  • クエリ: 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 を消すには、インデックスに何を含める必要があるか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「コードを変えていないのにバッチが3倍遅くなった」という状況を、原因から説明してください

与えられた条件
  • インデックスは存在している
  • 実行計画ではフルスキャンになっている
  • 前日から大量のデータが投入された

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この記事を前提にしている記事