実行計画の読み方 — 基礎編
実行計画の基礎。Seq Scan・Index Scan・各種 JOIN など RDBMS 共通の演算子と読み方。
実行計画とは
SQL は「何が欲しいか」を宣言的に書く言語で、「どうやって取るか」はオプティマイザが決めます。その「どうやって取るか」を木構造で書き出したものが実行計画(execution plan / query plan)です。
同じクエリでも、データ量・統計情報・インデックスの有無によって実行計画は変わります。遅いクエリの原因を探るときは、推測せずまず実行計画を見るのが鉄則です。
- PostgreSQL:
EXPLAIN/EXPLAIN ANALYZE - MySQL:
EXPLAIN/EXPLAIN ANALYZE(8.0 以降) - SQLite:
EXPLAIN QUERY PLAN - SQL Server:
SET SHOWPLAN_ALL ON/ SSMS の「実際の実行プラン」
スキャンの種類
テーブルから行を取り出す葉ノードにはいくつかのパターンがあります:
- Seq Scan / Full Table Scan(MySQL の
type=ALL): テーブル全行を先頭から走査。行数が少ない or 大半を読むなら合理的 - Index Scan(MySQL の
ref,range): インデックスを辿って行を特定し、テーブル本体から取得 - Index Only Scan(MySQL の
Using index): インデックスだけで完結(カバリング) - Bitmap Index Scan(PostgreSQL): 複数条件を合成してまとめて取得
「Seq Scan = 悪」ではありません。小さいテーブルや全件取得ではむしろ Seq Scan が最速です。オプティマイザは統計情報から行数を推定して選びます。
JOIN の種類
結合処理にも代表的な 3 種類があります。特徴を覚えておくと、「なぜこの計画が選ばれたか」が読めるようになります:
- Nested Loop Join: 外側テーブルの各行に対し内側をルックアップ。内側にインデックスがあり、外側が少行数のときに速い。相関サブクエリや小テーブル×大テーブルで選ばれやすい
- Hash Join: 片方でハッシュテーブルを作り、もう片方から探す。大きなテーブル同士・等値結合で強い。メモリを消費する
- Merge Join: 両方をソート済みにしておいて並行走査。両方がソート済み or インデックス順に取れるときに効率的
小テーブルが外側になっているのに Hash Join、等々「選択が不自然」なときは、統計情報が古いか、インデックスが足りていないサインです。
-- Nested Loop が選ばれる典型
-- (users 小さい × orders 大きい、orders.user_id にインデックス)
SELECT u.name, o.total
FROM users u JOIN orders o ON o.user_id = u.id
WHERE u.plan = 'enterprise';
-- Hash Join が選ばれる典型
-- (両方大きく、インデックスなしの等値結合)
SELECT a.x, b.y
FROM big_a a JOIN big_b b ON a.key = b.key;統計情報とコストモデル
オプティマイザは各演算子にコスト(推定 I/O + CPU 相当の数値)を付け、最もコストが低い計画を選びます。コストの基礎となるのが統計情報: 「この列にはユニーク値が何種類あるか」「値の分布はどうか」といったメタデータです。
統計情報は自動収集されますが、大量の INSERT / DELETE の直後や、大規模な ETL の直後には手動で更新したほうが良いことがあります:
- PostgreSQL:
ANALYZE table_name;(または自動 autovacuum) - MySQL:
ANALYZE TABLE table_name; - SQL Server:
UPDATE STATISTICS table_name;
統計情報が古いと、行数の推定値と実際値が大きくずれ、誤った計画が選ばれます。EXPLAIN ANALYZE で両者を比較するのが発見の鍵です。
計画は下から上へ読む
実行計画はインデントで表される木構造です。基本の読み方は:
- 最も深くインデントされた葉ノードから実行される
- 結果が親ノードに「流れる(feed)」
- ルート(一番上)が最終出力
「どこで時間がかかっているか」を探すときは:
- actual time / actual rows を葉から順に見る(PostgreSQL)
- 推定行数と実際行数の大きな乖離は統計情報の更新 or インデックス追加サイン
- 外側の Nested Loop の「rows × loops」が巨大なら、JOIN 順序の問題
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id)
FROM users u LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
-- 読み方:
-- HashAggregate <- 3. 集約
-- -> Hash Left Join <- 2. 結合
-- -> Seq Scan on users <- 1a. 葉 (左)
-- -> Hash
-- -> Seq Scan on orders <- 1b. 葉 (右)