チューニング

実行計画の読み方 — 基礎編

実行計画の基礎。Seq Scan・Index Scan・各種 JOIN など RDBMS 共通の演算子と読み方。

実行計画の読み方 — 基礎編 diagram

実行計画とは

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、等々「選択が不自然」なときは、統計情報が古いか、インデックスが足りていないサインです。

sql
-- 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 で両者を比較するのが発見の鍵です。

計画は下から上へ読む

実行計画はインデントで表される木構造です。基本の読み方は:

  1. 最も深くインデントされた葉ノードから実行される
  2. 結果が親ノードに「流れる(feed)」
  3. ルート(一番上)が最終出力

「どこで時間がかかっているか」を探すときは:

  • actual time / actual rows を葉から順に見る(PostgreSQL)
  • 推定行数と実際行数の大きな乖離は統計情報の更新 or インデックス追加サイン
  • 外側の Nested Loop の「rows × loops」が巨大なら、JOIN 順序の問題
sql
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. 葉 (右)