PostgreSQL | SQLite+MySQL経験者向け、30日で習得するPostgreSQL:設計とパフォーマンス - Day16 実行計画

SQL PostgreSQL
スポンサーリンク

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 ordersSeq 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 後半の着地点になる。

タイトルとURLをコピーしました