Day16 後半のゴール
「“実行計画を読んで、原因に当たりを付ける”ところまで行く」
前半で「EXPLAIN / EXPLAIN ANALYZE の意味」と「Seq Scan / Index Scan を見る」までは来ました。
後半では、もう一歩踏み込んで「JOIN や GROUP BY が絡む実行計画をざっくり読める」「どこを疑えばいいか当たりを付けられる」状態を目指します。
完璧に理解する必要はありません。
大事なのは、「このSQLが遅いとき、まずここを見る」という“自分なりのチェックポイント”を持つことです。
JOIN を含む実行計画の読み方
「“上から下へ”ではなく“インデントで親子関係を見る」」
JOIN が入ると、実行計画は一気に階層的になります。
例えば、こんなクエリを考えます。
SELECT
o.id,
o.ordered_at,
u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.ordered_at >= CURRENT_DATE - INTERVAL '30 days';
SQLこれに EXPLAIN ANALYZE をかけると、イメージとしてはこんな表示になります。
Hash Join (cost=... actual time=...)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=... actual time=...)
Filter: (ordered_at >= ...)
-> Hash (cost=... actual time=...)
-> Seq Scan on users u (cost=... actual time=...)
ここで大事なのは、「インデントで親子関係を見る」ことです。
一番上の Hash Join が“親ノード”で、その下に「左側の入力」「右側の入力」がぶら下がっている。Seq Scan on orders と Seq Scan on users が、それぞれのテーブルをどう読んでいるかを表している。
JOIN の種類(Hash Join / Nested Loop / Merge Join)はいったん置いておいて、
最初はこう考えれば十分です。
親ノード(JOIN や Aggregate)が“何と何を組み合わせているか”。
子ノードが“それぞれのテーブルをどう読んでいるか(Seq Scan か Index Scan か)”。
JOIN が遅いとき、いきなり JOIN の種類をいじるのではなく、
まず「子ノード側で無駄に全件読んでいないか?」を疑うのがセオリーです。
GROUP BY・集計を含む実行計画
「Aggregate ノードの“下”でどれだけ行が流れているかを見る」
集計系のクエリもよくボトルネックになります。
例えば、日別売上を出すクエリ。
SELECT
DATE(ordered_at) AS day,
SUM(quantity * unit_price) AS sales_amount
FROM order_items
GROUP BY DATE(ordered_at);
SQLこれに EXPLAIN ANALYZE をかけると、イメージとしてはこうです。
GroupAggregate (cost=... actual time=...)
Group Key: date(ordered_at)
-> Sort (cost=... actual time=...)
Sort Key: date(ordered_at)
-> Seq Scan on order_items (cost=... actual time=...)
ここで見るべきは、「Aggregate 自体」よりも、その“下流”です。
Seq Scan on order_items
→ まずテーブル全件を読んでいる。
Sort
→ GROUP BY のために、日付順に並べ替えている。
GroupAggregate
→ 並んだデータを日付ごとにまとめて集計している。
このパターンで遅いとき、「どこを疑うか」を整理するとこうなります。
order_items がそもそも巨大で、Seq Scan が重い。
Sort がディスクを使うほど大きくなっていて重い。
前者なら、「期間を絞る」「パーティションを切る」といった設計の話になるし、
後者なら、「インデックスであらかじめ並びを活かせないか」「集計粒度を変えられないか」といった検討になります。
実行計画は、「どのノードで何行流れているか」「どのノードで時間が跳ねているか」を見ることで、
“どこをいじるべきか”のヒントをくれます。
コストと実測値のズレを見る
「“見積もり行数”と“実際の行数”が大きく違っていないか」
EXPLAIN ANALYZE の出力には、だいたいこんな情報が付きます。
Seq Scan on order_items (cost=0.00..20000.00 rows=100000 width=...)
(actual time=0.050..500.000 rows=1000000 loops=1)
ここで注目したいのは、rows= の部分です。
rows=100000(コスト側)
→ プランナーが「このくらいの行数だろう」と予測した値。
rows=1000000(actual側)
→ 実際に流れた行数。
この差が大きいとき、PostgreSQL は「間違った前提」で実行計画を選んでいる可能性があります。
例えば、「10万行くらいだと思っていたら、実際は100万行だった」場合、
本当はインデックスを使った方が良かったのに、Seq Scan を選んでしまう、といったことが起きます。
このズレが大きいときに疑うべきなのは、統計情報です。
ANALYZE が十分に走っていない。
データ分布が偏っているのに、統計が古い。
などがあると、プランナーの見積もりが外れやすくなります。
「コストの rows と actual の rows が桁違いにズレていないか」を見るだけでも、
「これは統計の問題かもしれない」という当たりが付けられます。
よくある“遅いSQL”パターンを実行計画で見る
「WHERE 条件が効いていない JOIN」「LIMIT なのに重いクエリ」
現場でよく見る“遅いSQL”を、実行計画の目線で2つだけ挙げます。
ひとつ目は、「JOIN した結果に WHERE をかけている」パターンです。
SELECT ...
FROM big_table b
JOIN small_table s ON ...
WHERE b.status = 'active';
SQL実行計画を見ると、
大きいテーブル同士をまず JOIN
→ その後で WHERE で b.status を絞る
という順番になっていることがあります。
理想は、「b.status = ‘active’ の行だけを先に絞ってから JOIN」ですが、
インデックスや統計の状況によっては、そうなっていないことがある。
実行計画で、「どのタイミングで Filter がかかっているか」「JOIN の前に絞れているか」を見ると、
「WHERE の書き方やインデックス設計を変えるべきか」が見えてきます。
ふたつ目は、「LIMIT があるのに遅い」パターンです。
SELECT ...
FROM big_table
ORDER BY created_at DESC
LIMIT 10;
SQL実行計画を見ると、
Sort で全件を並べ替えてから LIMIT 10
になっていることがあります。
本当は、「created_at の降順インデックス」があれば、
インデックスから上位10件だけ取ってこれるはずです。
EXPLAIN ANALYZE で、
Sort ノードの actual time がやたら大きい
rows がテーブル全件になっている
といった状況が見えたら、「ORDER BY 用のインデックスを張るべきだな」という判断ができます。
実行計画を“怖くなくす”ための練習メニュー
「1本のクエリを、3パターンで比べてみる」
最後に、実行計画に慣れるためのシンプルな練習を提案します。
同じクエリについて、次の3パターンを試してみるイメージです。
インデックスなしの状態で EXPLAIN ANALYZE。
単純なインデックスを張って EXPLAIN ANALYZE。
複合インデックス(WHERE+ORDER BY を意識したもの)を張って EXPLAIN ANALYZE。
例えば、さっきのこれ。
SELECT *
FROM order_items
WHERE user_id = 123
ORDER BY ordered_at DESC
LIMIT 10;
SQLこれに対して、
インデックスなし。CREATE INDEX idx_order_items_user_id ON order_items(user_id);。CREATE INDEX idx_order_items_user_id_ordered_at ON order_items(user_id, ordered_at DESC);。
という3パターンで実行計画を比べてみる。
Seq Scan → Index Scan に変わる。
Sort が消えて、Index Scan だけになる。
cost や actual time がどう変わるか。
こういう“ビフォーアフター”を自分の目で見ると、
実行計画が一気に「ただの怖いテキスト」から「設計のフィードバック」に変わります。
Day16 後半のまとめ
JOIN や GROUP BY を含む実行計画は、「インデントで親子関係を追い、親ノード(Hash Join / Aggregate など)の“下”でどのテーブルがどう読まれているか(Seq Scan か Index Scan か、Sort が挟まっているか)を見る」ことから始めると、どこが重そうかが見えてくる。EXPLAIN ANALYZE では、「cost 側の rows(見積もり行数)」と「actual 側の rows(実際の行数)」のズレをチェックすることで、統計情報の問題やプランナーの誤解に気づけるし、「JOIN 前に WHERE で絞れていない」「LIMIT があるのに全件 Sort している」といった“よくある遅いSQLパターン”も、実行計画を通して具体的に見えるようになる。
最終的に、「1本のクエリに対してインデックスの有無・種類を変えながら EXPLAIN ANALYZE を比べ、Seq Scan → Index Scan、Sort の有無、time の変化を自分の目で確認する」ことが、実行計画を“怖いもの”から“設計の相棒”に変える Day16 後半の着地点になる。
