なぜ、そのWHERE句は「そこ」にあるのか —— 述語プッシュダウンの深淵
PostgreSQLのクエリチューニングと向き合っていると、時折「なぜこのコスト見積もりになったのか」と頭を抱える瞬間があるはずだ。特に結合(JOIN)やサブクエリが複雑に絡み合う時、クエリオプティマイザが賢明な判断を下しているのか、それとも我々の書き方が彼らの足を引っ張っているのかを見極めるのは、熟練のエンジニアにとっての醍醐味と言える。
今日は、クエリ最適化の基本にして奥義、「述語のプッシュダウン(Predicate Pushdown)」について、少しだけ内部の挙動に踏み込んで話してみたいと思う。
述語プッシュダウンが果たす役割
言葉の定義をわざわざ説明するまでもないかもしれないが、述語プッシュダウンの本質は「不要な行を、可能な限り早い段階で切り捨てること」にある。
例えば、VIEWやサブクエリ、あるいはWITH句(CTE)越しに条件を渡す際、オプティマイザがその条件をスキャンレベル(テーブルアクセス時)まで押し下げてくれるかどうかで、実行計画は劇的に変わる。もしプッシュダウンが効かなければ、データベースは「まずは全てのデータを結合して、その後にフィルタリングする」という、物理的にもメモリ的にも非常に贅沢な処理を行うことになる。これは、大規模データセットを扱う環境では致命傷になりかねない。
アーキテクチャの視点:なぜ「壁」にぶつかるのか
PostgreSQLのプランナは、非常に洗練されているが、魔法ではない。ある種のクエリ構造に出くわすと、プッシュダウンが阻害されることがある。
特に注意が必要なのが、「ブラックボックス化」だ。
- 複雑な関数や非決定的な演算子: 例えば、`WHERE f(column) = 1` という記述。関数 `f` が `IMMUTABLE` でなければ、オプティマイザはインデックスを有効活用できず、プッシュダウンの判断を躊躇することがある。
- 外部結合(OUTER JOIN)の制約: 左外部結合の右側テーブルに対する条件を `ON` 句ではなく `WHERE` 句に書いた場合、プランナはその条件を結合後に適用せざるを得ない(NULLが生成される可能性があるため)。これを理解せずに「なぜ述語が下に落ちないんだ?」と悩むエンジニアは多い。
- CTEのバリア(PostgreSQL 12以前): 以前のバージョンでは、WITH句は最適化の境界となっており、述語が内側にプッシュされないことがあった。最新のバージョンでは大幅に改善されたが、それでも複雑な依存関係がある場合には、オプティマイザが安全側に倒してプッシュダウンを避けるケースは今も存在する。
実践的なトラブルシューティング:プランナの「意図」を読み解く
現場で「遅いクエリ」に遭遇した時、私がまず行うのは `EXPLAIN (ANALYZE, BUFFERS)` を叩くことだ。ここで見るべきは、単なる実行時間ではない。
1. `Filter` と `Index Cond` の違い: `Filter` が表示されている場合、それはスキャン後にメモリ上でフィルタリングされている証拠だ。もしその列にインデックスがあるなら、なぜそれが `Index Cond` になっていないのか。述語が適切にプッシュされていない可能性を疑うべきだ。
2. `Rows Removed by Filter` の数値: この値が大きいほど、無駄なI/Oが発生しているということだ。ここでコストを浪費しているなら、述語を結合句に移動させるか、あるいはクエリの構造そのものを簡素化して、プランナが最適化しやすい道を作ってやる必要がある。
最後に:オプティマイザと「対話」する
クエリチューニングは、オプティマイザとの対話だ。彼らは論理的には正しいが、時には統計情報の不足や、我々の記述したクエリの複雑さに迷うことがある。
「述語をプッシュダウンさせる」という意識を持つことは、単にクエリを速くするだけではない。データベースが本来持つべき「最小限のデータで最大限の成果を出す」というアーキテクチャの原則に寄り添うことでもある。
もしあなたのクエリが期待通りの性能を出していないなら、今一度プランを見てほしい。その述語は、本当に「最も早い段階」で適用されているだろうか?
機械が解釈しやすいクエリを書くこと。それは、エンジニアがデータベースという巨大なエンジンに対して敬意を払うための、最も洗練された方法なのだから。
コメント