サブクエリの「平坦化」を攻略する:PostgreSQLクエリプランナの隠れた知能
PostgreSQLを長く触っていると、ふとした瞬間にプランナの「機嫌」に振り回されることがありますよね。同じようなロジックなのに、書き方一つで実行計画が激変する。その最たる例が「サブクエリの平坦化(Subquery Flattening)」です。
今回は、PostgreSQLが裏側でどうやって複雑なサブクエリをJOINに解体しているのか、そしてなぜ時としてその魔法が解けてしまうのか、深掘りしていこうと思います。
なぜプランナは平坦化をしたがるのか
PostgreSQLのオプティマイザにとって、サブクエリは「不透明なブラックボックス」になりがちです。特に `FROM` 句に置かれたサブクエリ(Subquery in FROM)は、そのままでは独立した中間結果セットとして扱われ、コスト計算の精度を落とす原因になります。
そこでプランナは、サブクエリを元のクエリ本体と結合させ、一つの巨大な「JOINの木」として再構成しようとします。これが「平坦化(Subquery Flattening / Pull-up)」です。
これが成功すると、サブクエリ内のテーブルと外側のテーブルが、あたかも最初から同じスコープに存在していたかのように、Nested LoopやHash Joinの選択肢が格段に広がります。インデックスの利用範囲も広がり、プルーニングの効率も劇的に向上します。
「平坦化」が阻害される時
しかし、どんなサブクエリでも平坦化できるわけではありません。プランナが「これ以上は弄れない」と白旗を上げる条件がいくつか存在します。ここを理解しておかないと、トラブルシューティングで迷路に迷い込むことになります。
特に注意すべき「阻害要因」は以下のケースです。
- 集合演算の介在: `UNION`, `INTERSECT`, `EXCEPT` が含まれる場合、プランナはこれらを単なるJOINに展開できません。これらは「マテリアライズ(一時テーブルへの書き出し)」のフラグとなります。
- LIMIT / OFFSETの存在: サブクエリ内で `LIMIT` が指定されていると、その順序や件数が保証されなければならないため、プランナは安易に結合を試みません。
- GROUP BY / HAVING: かつてのPostgreSQLではこれらも平坦化の大きな障壁でしたが、近年のバージョンではかなり改善されました。それでも、集約関数の複雑さによっては平坦化が諦められることがあります。
- DISTINCT: これもまた、順序や一意性の制約から、プランナにとって平坦化を難しくする要因の一つです。
パフォーマンストラブルの現場で
現場でよく遭遇するのは、「なぜかこのクエリだけインデックスが使われない」というケースです。
`EXPLAIN` を叩いてみて、`Subquery Scan` という文字列が見えたら要注意です。それは「平坦化に失敗し、サブクエリを先に実行して結果をメモリ上に展開した」というプランナからのメッセージです。
この時、もしサブクエリの内側で数百万件を処理し、外側でさらに結合を行っているなら、メモリ不足によるディスクへのスピル(一時ファイルへの書き出し)が発生している可能性が高い。
解決のヒント:
もしそのサブクエリが単純な構造であれば、思い切って `WITH` 句(CTE)を使わずに直接JOINへ書き直すか、逆に、CTEが邪魔をしているなら `MATERIALIZED` / `NOT MATERIALIZED` ヒント句(PostgreSQL 12以降)を使ってプランナに明示的に指示を送るのも手です。
最後に:プランナを信頼しつつ、疑う
PostgreSQLのプランナは非常に優秀ですが、人間が意図した「データ分布の特性」までは完全には理解できません。
「サブクエリを書く」ということは、プランナに対して「まずこの範囲を計算してくれ」という指示を出しているのと同じです。もし実行計画が期待通りにならないなら、それはプランナが平坦化を諦めているか、あるいは平坦化しない方がコストが低いと誤認しているかのどちらかです。
複雑なクエリを書くときは、常に `EXPLAIN (ANALYZE, BUFFERS)` を見て、「期待通りにJOINが展開されているか」を確認する癖をつける。これだけで、データベースエンジニアとしての解像度は一段階上がります。
皆さんのクエリが、今日も効率的なJOINプランを選択してくれることを願っています。それでは、また。
コメント