
2026/10/07 7:09
クエリ変換パイプライン
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Readyset は、クエリをデータフローグラフにコンパイルし、結果を増分的に維持することでデータベースキャッシングを革新し、従来のコストベースエンジンに見られる実行ごとに再実行を行う必要を取り除きます。このアプローチは関連するサブクエリや非等価条件・外部結合述語などの特定の SQL 機能の使用を制限しますが、ランタイム最適化ではなく決定論的な構造アルゴリズムを用いることで、マルチパスの書き換えパイプラインが任意の SQL を厳格なデータフロー要件に橋渡しします。パイプラインは正規化から始めます:構文のデシュガーリング、メタデータの複製によるスキーマ解決、スタークエリの展開、カラムの資格認定(qualification)、
USING 節を ON 述語に変換する。その後、特定順序での深い書き換えを実行します。これには、Array Constructor Rewrite(PostgreSQL の ARRAY(SELECT ...) を LATERAL LEFT JOIN と array_agg に変換)、冗長結合の排除、左スボーンホイスト、サブクエリdecorrelation(関連付けられた述語を維持可能な結合条件に変換)、3VL ロジック処理による IN/NOT IN の処理(NP および EP プローブを使用)、導出テーブルのインライン化、述語への近接性に基づく結合順序の再配置によるキャッシュ共有のための正規シーケンスの生成、そしてフィルタホイストが含まれます。 Nullable 解析は静的に不要なプローブを除去し、冗長な結合や状態を防ぎます。構造的 ProbeRegistry は同一のサブクエリボディに対するコンパイル済み形式を共有することで、レージーアップグレードをサポートします。節の正規化は位置とエイリアス参照を解決し、信頼できるセマンティックフィンガープリンティングを可能にします。これにより、データフローコンパイラーが要求するすべての構造不変条件を満たす最終出力が確保され、ストリーミングオペレーターへの変換が行われます。この堅牢なコンパイルにより、計算オーバーヘッドを大幅に削減した永続クエリキャッシュが可能になり、冗長な状態管理作業を防ぎます。本文
データフローにおけるクエリ書き換えの重要性
1. 従来のデータベースと Readyset の違い
多くの関係型データベースは「プル(要求ベース)」モデルを採用しています。
- 実行メカニズム:クエリが到着するたびに、エンジンが最初から実行計画を構築し、テーブルのスキャンや結合・フィルタリングを行います。
- 課題:読み込みレイテンシーは、クエリの複雑さとアクセスするデータ量の比例して増大します。
これに対して Readyset は「データフロー」アプローチを採用しています。
- コンパイル方式:オンデマンドの再実行ではなく、各クエリを「データフローグラフ」にコンパイルします。
- 更新伝播:基盤データの变化(挿入・更新・削除)がグラフ全体に伝播し、キャッシュされた結果が段階的に維持されます。読み込みは事前計算したマテリアライズドビューへのルックアップとなります。
- 最適化のタイミング:
- 従来のエンジン:各実行時にクエリを最適化します。
- Readyset:キャッシュ生成時のみ一度だけ最適化し、その後結果を維持します。
重要:このアプローチのためには、データフローエンジンがコンパイルできる厳格な形式で SQL を表現する必要があります。SQL の宣言型特性(等価な書き方の多様性)と対立する制約が存在します。
2. データフローエンジンが必要とする制約
Readyset のデータフローコンパイラは、従来のオプタイマとは異なり、以下の構造制約を課します。これらが満たされない場合、PostgreSQL や MySQL で正常動作する SQL もエラーになります、または非効率的なグラフにコンパイルされます。
-
等値述語によるバイナリ結合
- 各結合は列の等値述語(
)を通じて恰好 2 つの入力を接続する必要があります。a.id = b.id - ハッシュベースの結合状態を維持します。
- 対応しない仕様:レンジ述語、式ベースの結合キー、または複数テーブルの ON 条件は直接サポートされません。
- 各結合は列の等値述語(
-
相関実行なし
- データフローグラフには「外部行ごと」という概念がありません。各オペレーターは完全な変更ストリームを認識します。
- そのため、相関サブクエリは等価な結合に書き換えられる必要があります。
-
フラットな結合構造の推奨
- 派生テーブル(FROM 節内のサブクエリ)はサポートされますが、コストがかかります。
- パラメータ化できない完全マテリアライゼーションとしてコンパイルされ、外部クエリとは異なる中間結果を生成します。
- インライン化推奨:書き換えパイプラインは、意味論的に安全な限り派生テーブルをインライン化(FROM、WHERE、投影の吸収)し、直接のパラメータ化されたルックアップを構築することを好みます。
-
サポートされる結合タイプ
- 対応:INNER JOIN、LEFT OUTER JOIN、CROSS JOIN。
- 非対応:RIGHT JOIN、FULL OUTER JOIN(両側からのマッチ欠失の追跡がストリーミングでは困難であるため)。
-
集計境界
句と集計関数はステートフルオペレーターとしてコンパイルされます。GROUP BY- 少なくとも 1 つの集計派生表現を投影することが求められ、
キーは明示的な列参照(位置番号やエイリアスは不可)である必要があります。GROUP BY
3. 書き換えパイプライン:SQL からデータフローへの変換
ユーザーが
CREATE CACHE を発行すると、Readyset はクエリを以下の 3 つのブロックからなるマルチパスの書き換えパイプラインに通します。これにより、標準形式の SQL からデータフローエンジンが期待する形式に変換されます。
| ブロック | 目的 | 処理例 |
|---|---|---|
| A — ノーマライゼーション | 構文を簡略化、スキーマを解決、列を限定する | → 明示的な列リストへ |
| B — ディープ書き換え | サブクエリを相関解除、派生テーブルをフラット化、最適化 | IN サブクエリ → JOIN への変換 |
| C — クリーニング | 冗長な節を削除、リテラルをパラメータ化する | 単一行の結果確定時、 の削除 |
各パスは意味を保証しており、書き換え後のクエリはあらゆるデータに対して元のクエリと同じ結果を返します。
ブロック A:SQL を標準化する
予測可能な形状に変換し、後続の処理を可能にします。
- スキーマ解決:テーブルおよび列名を実際のスキーマメタデータ(プライマリキー、ユニーク制約、列タイプなど)にバインドします。
- スター展開:
を明示的な列リストに置換します。ワイルドカードではなく命名された列のみが許容されます。SELECT * - 列限定:各列参照をテーブル名で前缀付けします(例:
→id
)。複数のテーブルでの曖昧さを防ぎます。t.id - USING の簡略化:
をJOIN ... USING(id)
に変換します。JOIN ... ON (a.id = b.id)
ブロック B:本格的作業
データフローエンジンが処理できない構文を、等価な処理可能な構文へ変換します。
① Array Constructor Rewrite(配列コンストラクタ書き換え)
PostgreSQL の
ARRAY(SELECT ...) は、「外部行ごとのスカラー集計」形状であり、直接的なデータフローオペレーターではありません。
- 変換先:
をラップしたarray_agg(...)
へ書き換えられます。LATERAL LEFT JOIN - 追加処理:空のサブクエリが NULL らず配列を生み出すように、外側で
を適用します。COALESCE(..., ARRAY[])
-- Before: 配列コンストラクタ(直接対応不可) SELECT u.name, ARRAY(SELECT p.title FROM posts p WHERE p.user_id = u.id) AS post_titles FROM users u -- After: LATERAL JOIN + array_agg (対応可能) SELECT u.name, COALESCE(array_subq.agg_result, ARRAY[]) AS post_titles FROM users u LEFT JOIN LATERAL ( SELECT array_agg(inner_subq.title) AS agg_result FROM (SELECT p.title FROM posts p WHERE p.user_id = u.id) inner_subq ) array_subq ON TRUE
② 冗残結合の削除
- ORM が生成したクエリにありがちな「主キーに基づく自己結合」や、投影される列が一方のみから来る結合を早期に検出し削除します。
- これにより、後続の変換前の結合グラフの複雑性を低減します。
③ Left-Spine Hoisting(左脊起上げ)
- 目的:FROM 節内の最左側の派生テーブルをインライン化し、Top-K パターン(
)をトップレベルへ持ち上げます。ORDER BY ... LIMIT - 効果:
- Top-K は
ベースのフィルタから、データフローコンパイラのネイティブなストリーミングオペレーターに展開されます。ROW_NUMBER()
をパラメータ(LIMIT
)として残すため、?
などの変種を単一のキャッシュグラフで扱えます。LIMIT 10
- Top-K は
-- Before: Top-K が派生テーブル内にネスト(非効率的) SELECT sq.id, sq.name, sq.score FROM (SELECT id, name, score FROM products ORDER BY score DESC LIMIT ?) AS sq -- After: Left-Spine Hoist によりトップレベルへ持ち上げ(効率的) SELECT id, name, score FROM products ORDER BY score DESC LIMIT ?
④ サブクエリの相関解除
最も複雑な変換であり、既存のパスが準備してくれた構造を利用します。
- 例:
のような相関サブクエリ。WHERE o.total > (SELECT AVG(total) FROM orders WHERE region = o.region) - 変換方針:外部行ごとの実行モデルがないため、結合(INNER JOIN)に変換されます。
でグループ化し、GROUP BY
を作成することで、平均値を段階的に維持するようにコンパイルします。ON 条件
- 対応パターン:
、EXISTS
、NOT EXISTS
、IN
、スカラーサブクエリ、NOT IN
など。LATERAL JOIN
⑤ 三値論理(3VL)による IN, NOT IN の処理
SQL は
TRUE、FALSE、NULL の 3 状態を持つ三値論理(3VL)を使用します。NULL を生じうるサブクエリに対する IN/NOT IN の扱いを述語の位置別に整理します。
- WHERE 節内の NOT IN
- RHS が単一の NULL を生成する場合、本来 FALSE だった述語が NULL になり、行が除外されてしまいます。
- 単純な LEFT ANTI JOIN に置換するとこの区別が失われるため、**プロブジョイン(Probe Join)**が必要になります。
- WHERE 節内の IN
- FALSE(一致なし)と NULL(一致なしだが RHS に NULL あり)はどちらも行破棄のため、単純な SEMI-JOIN で対応可能です。
- SELECT リスト内の IN / NOT IN
- 常に 3VL ガードが必要です。投影された値が他の式と比較される可能性があるためです。
実装方法:プロブジョイン 相関解除されたサブクエリの横に以下のようなプロブジョインを追加します。
- NP (null-present):
——RHS が NULL を生じるか?EXISTS(rhs WHERE first_field IS NULL) - EP (existence):
——RHS は非空か?EXISTS(rhs)
これらは下流述語が
IS [NOT] NULL でテストするための LEFT LATERAL JOIN としてマテリアライズされます。
最適化:各プロブは追加の JOIN を意味するため、NULL アビリティ分析を行い不要なプロブを抑制します(例:RHS が NULL 自由であることが証明された場合、NP プロブはスキップ)。同型のプロブは
を使って再利用されます。ProbeRegistry
⑥ 一般派生テーブルのインライン化
- 目的:相関解除後残る派生テーブルを外部結合構造にフラット化し、中間マテリアライゼーションを排除します。
- 例:集計計算を行ってから外部でフィルタリングするケースは、
をトップレベルへ持ち上げ(外部 WHERE → HAVING に移行)、効率的なグラフに変換します。GROUP BY
- 例:集計計算を行ってから外部でフィルタリングするケースは、
- 制限:ウィンドウ関数や自己結合など、意味論を変化させる構造には適用されません。
⑦ 結合順序の再配置
- 課題:構文的に異なった結合順序(
vsJOIN a JOIN b
)が冗長なキャッシュを生むことがあります。JOIN b JOIN a - アプローチ:コストベースではなく、決定論的かつ構文独立な結合シーケンスを生成します。
- 述語接近度や辞書順でスコアリングし、標準化された順序へ統一します。
⑧ 意味論的フィンガープリンティングのための節のノーマライゼーション
- 位置参照とエイリアスの解決:
をGROUP BY 1
、GROUP BY t.name
をORDER BY total
のように明示的な列参照へ変換します。ORDER BY SUM(t.amount) - これにより、構造的に同一なクエリが信頼性の高いキャッシュヒットを実現します。
⑨ フィルタ起上げ
- 処理:派生テーブル内のパラメータ化されたフィルタ(
)を最外層の WHERE 節へ起上げます。WHERE id = ? - 理由:トップレベルでのデータフローエンジンによるキーベースルックアップが最も効率的です。
ブロック C:最終クリーニング
変換後のクエリから冗長な要素を除去します。
- ORDER BY / LIMIT の削除:ユニークキーに基づくフィルタリングなどで結果が 1 行以下であることが証明された場合、ソートや制限は不要です。
- リテラルのパラメータ化:構造的に同一な定数値をパラメータへ置き換えます。
4. 結果とメリット
書き換えパイプラインを終えたクエリは、以下のような標準形になります。これによりデータフローエンジンが直接コンパイルでき、すべての構造的不変条件を満たします。
- すべてのサブクエリが結合へ相関解除されました。
- すべての派生テーブルがインライン化(またはサポートされたオペレーターとして残存)されました。
- 列参照は完全に限定され、結合述語は列等値対に揃いました。
- 結合順序は標準化され、冗残な節は削除されました。
- パラメータ化されたフィルタはトップレベルに置かれました。
結論: データフローコンパイラはこれをストリーミングオペレーターのグラフへ翻訳し、すべてのデータ変更がグラフを通じて流れ、元からクエリを再実行することなくキャッシュ結果を更新し続けます。
- 高速化の原理:再度実行する必要がなくなることで達成されます。
5. 今後の仕事
現在のパイプラインは汎用的な書き換えに基づいていますが、より高度な最適化に向けた次のステップがあります。
- コストベース結合再配置の実装
- 現在の Join Reordering は標準化のみですが、統計情報を用いた真のコストベースオプタイマ(CBJO)を追加予定です。
- ガードレールの緩和
- よりターゲティングされた分析が安全に行えるよう、ウィンドウ関数や複合キーのチェックなどを慎重すぎない設定へ変更します。
- パターン特化型変換の導入
- 現在の汎用ヒューリスティックでは対応できない特定のクエリ形状(ORM や BI ツール特有のパターンなど)に対して、個別の書き換えパスを追加します。
このように、独立で組成可能かつ意味を保証するパイプラインアーキテクチャにより、機能を段階的に追加・検証することが可能です。