述語プッシュダウンと射影プッシュダウンの最適化
WHERE と SELECT 列をデータ源の近くで先に絞るだけで、読む I/O が桁で減る理由が分かります。論理最適化の原理と列指向・分散・外部表での削減効果を原理から押さえられます。
- 1.述語プッシュダウンは選択(フィルタ)をできるだけ走査側へ押し下げ、射影プッシュダウンは不要列を早く落とす。どちらも結果を変えない関係代数の等価変換で、中間結果の行数と幅を上流に渡る前に削る。
- 2.効果はデータ源が「絞り込みを肩代わりできる」ほど大きい。列指向は読む列ブロックを減らし、分散・外部表はネットワーク転送を減らし、Parquet 等はメタデータで読まないブロックを丸ごと飛ばせる。
- 3.押し下げには等価性の制約がある。外側結合・集約・非決定的関数・NULL 意味論を越えると結果が変わるため、オプティマイザは規則で安全な範囲だけ押し下げる。
「いつ・どこで絞るか」が I/O を決める
同じ結果を返すクエリでも、フィルタと列の絞り込みを演算子木のどこで行うかで読むデータ量は桁違いに変わります。述語プッシュダウン(predicate pushdown) はフィルタ(関係代数の選択 σ)を、射影プッシュダウン(projection pushdown) は列の絞り込み(射影 π)を、それぞれ可能な限りデータ源に近い側へ押し下げる論理最適化です。どちらも結果を変えない等価変換で、上流の結合・集約に行が届く前に、行数(縦)と行の幅(横)を削ります。これは クエリオプティマイザの内部 が物理プランを選ぶ前の、論理プラン整形の中核です。
原理:関係代数の等価規則
押し下げは関係代数の恒等式に基づきます。代表的なのは選択を結合の下へ通す規則です。
# 述語 p が表 R の列だけを参照するとき
σ_p(R ⋈ S) = σ_p(R) ⋈ S # p を結合の手前で R に適用してよい
# 射影:上位で使う列集合だけ残して下へ
π_cols(R ⋈ S) = π_cols( π_{R必要列}(R) ⋈ π_{S必要列}(S) )
左辺は「結合してから絞る」、右辺は「絞ってから結合する」。結果の集合は等しいのに、右辺は結合に入る入力が小さくなります。素朴な実行は左辺の順で動くため、オプティマイザが右辺へ書き換えること自体が最適化です。射影も同様に、最終的に出力されない列を走査の直後で捨てれば、中間タプルの幅が縮み、コピー量とメモリ消費が減ります。
押し下げの理想は、フィルタと列選択がスキャン演算子そのものに融合し、走査しながら不要な行・列を初めから読まない状態です。中間ノードに残ったフィルタは、いったん読んだ行を捨てるだけで I/O は減りません。本当の利得は、絞り込みがストレージ層の読み取り判断に届いたときに生まれます。
列指向での効き方:射影が I/O を直接削る
行指向と列指向 のうち、射影プッシュダウンが最も直接的に効くのは列指向です。列指向は同一カラムの値を連続配置するため、SELECT amount FROM t WHERE status='paid' では amount 列と status 列のブロックだけを読み、他の列ブロックには一切触れません。
- 射影:全
N列のうちk列しか参照しないなら、読むブロックは素朴にk / Nの割合まで減る。広い表で少数列を集計する OLAP では支配的な差になる。 - 述語:
status='paid'を辞書符号化された ID への整数比較に変換し(→ テーブル圧縮の符号化)、合格行のビットマップを作ってからamountを取り出す。落ちる行のamountは読まずに済む。
この「述語で先に行を絞り、必要列を後から実体化する」順序が レイトマテリアライゼーション で、述語・射影プッシュダウンが実行モデルと噛み合う典型例です。
ブロック単位スキップ:メタデータで丸ごと飛ばす
押し下げのもう一段上が、読む前に読まずに済むと判断する仕組みです。Parquet や ORC、列指向エンジンはデータを行グループ/ブロックに分け、各ブロックの最小値・最大値などの統計をメタデータに持ちます。押し下げられた述語をこの統計と突き合わせ、条件に合致し得ないブロックを丸ごとスキップします。
ブロックA: min(ts)=2024-01-01, max(ts)=2024-01-31
ブロックB: min(ts)=2024-02-01, max(ts)=2024-02-28
述語: ts >= 2024-02-10
→ ブロックA は max が 1/31 で条件を満たし得ない → 読まずにスキップ
→ ブロックB だけ読む
これは min/max プルーニング(zone map) と呼ばれ、述語プッシュダウンがあって初めて成立します。述語がスキャンに届かなければ、エンジンはどのブロックが不要か判断できません。同じ発想がパーティション単位で働くのが パーティションプルーニングで、パーティショニング のキー列に述語が押し下がると、無関係なパーティションのファイル自体を開きません。粒度はパーティション、行グループ、ブロックと段階的に効きます。
WHERE ymd = '2024-02-10' はパーティションキー列に直接かかるためプルーニングが効きますが、WHERE date_trunc('month', ts) = ... のように列を関数で包むと、多くのエンジンは min/max と突き合わせられずスキップを諦めます。押し下げを活かすには、述語をキー列そのものへの単純比較として書くのが要点です。
分散・外部表:転送量を源で削る
分散クエリや外部表(フェデレーション)では、押し下げは I/O だけでなくネットワーク転送を削ります。各シャードやリモートのデータ源で先に絞れば、ノード間を流れる行が減るからです。
| 押し下げない | 押し下げる | 削れるもの |
|---|---|---|
| 全行・全列を取り寄せてから絞る | リモートで σ・π を適用して結果だけ返す | ネットワーク転送量・直列化コスト |
| 集約をコーディネータで実行 | 部分集約をシャードで先に計算 | 中間結果の行数・集約ノードの負荷 |
| JOIN をすべて中央で実行 | 結合キーで絞った行だけ集める | シャッフルされるデータ量 |
外部表ラッパ(PostgreSQL の FDW、Spark/Trino のコネクタなど)の性能は「どこまで述語と射影をリモートへ押し下げられるか」でほぼ決まります。押し下げできない述語はローカルで評価するしかなく、その分だけ全行をネットワーク越しに引いてから捨てる無駄が生じます。実行計画に Remote Filter や pushdown が表示されるかは、ここで効いているかの直接の指標です。
押し下げてはいけない場合:等価性の境界
押し下げは結果を変えないことが大前提です。次のような境界を越えると結果が変わるため、オプティマイザは規則で安全な範囲だけ押し下げます。
| 状況 | なぜ押し下げ不可(または制約) | 扱い |
|---|---|---|
| 外側結合の NULL 生成側への述語 | 結合後に NULL になる行へ条件をかけると、先に適用すると結果集合が変わる | ON 句と WHERE で意味が異なる。WHERE は外側結合を内側結合へ縮退させ得る |
| 集約より上の述語(HAVING) | GROUP BY 後の集約値(COUNT等)への条件は集約前に評価できない | グループ列への条件のみ WHERE へ降ろせる |
| 非決定的・副作用のある関数 | random() や評価順依存の関数は適用回数・位置で結果が変わる | 押し下げず元の位置で評価 |
| 三値論理(NULL)を変える書き換え | 述語の合成や否定の押し下げで NULL の扱いがずれることがある | NULL 安全性を保つ規則のみ適用 |
特に外側結合は要注意です。LEFT JOIN ... ON の条件は結合の前に右表へ押し下げられますが、結合結果に対する WHERE の条件を NULL 生成側へ無条件に押し下げると、本来 NULL 行として残るべき行が落ち、外側結合が実質的に内側結合へ変わってしまいます。オプティマイザはこの NULL 拒否性を判定し、安全なときだけ降ろします。
押し下げは万能の前進規則ではありません。等価性が崩れる境界(外側結合の NULL、集約、ウィンドウ関数、非決定的関数)では止めるのが正しく、止めなければバグです。最適化の目的はあくまで「結果を保ったまま」読む量を減らすこと。等価性が優先で、速度はその制約下での話です。
述語プッシュダウン=選択 σ を走査側へ、射影プッシュダウン=不要列を早期に落とす、いずれも結果を変えない等価変換と即答できるように。効く理由は(1) 結合・集約に渡る前に行数と幅を削る、(2) 列指向では読む列ブロックが減る、(3) Parquet 等の min/max でブロックを丸ごとスキップ、(4) 分散・外部表ではネットワーク転送が減るの4点。外側結合の NULL 生成側・集約・非決定的関数では押し下げると結果が変わるため止める、まで言えると確実です。
まとめ
述語プッシュダウン(σ を走査側へ)と射影プッシュダウン(π を早期に)は、結果を保ったまま中間結果の行数と幅を削る関係代数の等価変換です。列指向では読む列ブロックが減り、Parquet/ORC の min/max 統計やパーティションキーへの述語でブロック・ファイルを丸ごとスキップでき、分散・外部表ではリモートで先に絞ることでネットワーク転送が減ります。利得は絞り込みがスキャン演算子やストレージ層に届くほど大きく、中間ノードに残ったフィルタは I/O を減らしません。一方で外側結合の NULL 生成側・集約・非決定的関数を越える押し下げは結果を変えるため、オプティマイザは等価性を保つ規則の下でだけ押し下げます。EXPLAIN に Filter がスキャン段へ降り、Rows Removed by Filter やリモートの pushdown が見えるかが、効いているかの直接の手掛かりです。
データベース Article
述語プッシュダウンと射影プッシュダウンの最適化を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
述語プッシュダウン
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
効果はデータ源が「絞り込みを肩代わりできる」ほど大きい。列指向は読む列ブロックを減らし、分散・外部表はネットワーク転送を減らし、Parquet 等はメタデータで読まないブロックを丸ごと飛ばせる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「述語プッシュダウン / 射影プッシュダウン」に近いか確認する。
- 強みである「述語プッシュダウンは選択(フィルタ)をできるだけ走査側へ押し下げ、射影プッシュダウンは不要列を早く落とす。どちらも結果を変えない関係代数の等価変換で、中間結果の行数と幅を上流に渡る前に削る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。