プログラミング
クエリ変換パイプライン
The Query Transformation Pipeline (readyset.io)
要約
Readysetは、従来のデータベースがクエリごとに実行計画を再構築するのに対し、クエリをデータフローグラフにコンパイルして結果を継続的に維持するアプローチを採用しています。このデータフローエンジンはSQLの構文に制約があるため、ReadysetはSQLをデータフローエンジンが期待する形式に変換する多段階の書き換えパイプラインを使用しています。このパイプラインは、SQLの正規化、サブクエリのデコレレーション、派生テーブルのインライン化などを実行し、データフローエンジンが効率的に処理できる形にSQLを整形します。
全文翻訳
Readysetのエンジニアリング
ReadysetがSQLを書き換える方法:クエリ変換パイプラインの内部
なぜクエリ書き換えがデータフローにとって重要か
ほとんどのデータベースは同じように動作します。クエリが到着すると、エンジンは実行計画を構築し、テーブルをスキャンし、行を結合し、フィルタリング、集計を行い、結果を返します。クエリが実行されるたびに、その作業はゼロから繰り返されます。このプルベースのモデルはリレーショナルデータベースに数十年間使われてきましたが、固有のコストが伴います。読み込みレイテンシは、クエリの複雑さとそれが触れるデータのサイズに比例します。
Readysetは根本的に異なるアプローチを取ります。クエリをオンデマンドで再実行するのではなく、Readysetは各クエリをデータフローグラフにコンパイルします。これは、基盤となるデータが変更されるにつれてクエリの結果を継続的に維持する演算子のネットワーク(結合、フィルタ、集計、射影)です。アップストリームデータベースで行が挿入、更新、または削除されると、変更はグラフを伝播し、キャッシュされた結果は増分的に更新されます。読み込みは、完全なクエリ実行ではなく、事前計算されたマテリアライズドビューへのルックアップになります。これが重要な違いです。従来のエンジンは、クエリが実行されるたびにクエリを実行する方法を最適化します。Readysetは、キャッシュ作成時に一度だけ最適化し、その後、結果を永続的に増分的に維持します。
トレードオフは、クエリがデータフローエンジンでコンパイルできる形式で表現されなければならないということです。そして、その形式はSQLが許可するものよりも制限的です。
データフローエンジンが必要とするもの
SQLは宣言型言語です。同じ論理クエリは、相関サブクエリ、派生テーブル、CTE、LATERAL結合、ネストされた集計など、多くの同等な方法で記述できます。従来のオプティマイザは、これらを相互に交換可能な表現として扱い、構文に関係なく最適な実行計画を選択します。
Readysetのデータフローコンパイラは、従来のオプティマイザではありません。SQLをストリーミング演算子の有向非巡回グラフに変換します。各演算子は入力から変更を受け取り、出力にそれらの変更を発行します。このアーキテクチャは、SQL構文にはない構造的な制約を課します。
バイナリ結合と等価述語。データフローグラフ内の各結合は、列の等価性述語(a.id = b.id)を介して正確に2つの入力を接続します。エンジンはこれらの等価性を使用してハッシュベースの結合状態を維持します。範囲述語、式ベースの結合キー、または複数テーブルのON条件は直接サポートされていません。
相関実行なし。従来のエンジンでは、相関サブクエリは外部行ごとに1回実行されます(ネストループパターン)。データフローグラフには「外部行ごと」という概念はありません。各演算子は、入力からの変更の完全なストリームを参照します。相関サブクエリは、データフローが増分的に維持できる同等の結合に書き換える必要があります。
フラットな結合構造が好ましい。派生テーブル(FROM句のサブクエリ)はデータフローエンジンでサポートされていますが、コストがかかります。派生テーブルは、パラメータ化できない完全にマテリアライズされた中間ノードにコンパイルされます。入力データの各異なる組み合わせは、外部クエリが必要とするかどうかにかかわらず、格納された結果を生成します。派生テーブルをインライン化する(派生テーブルのFROM項目、WHEREフィルタ、射影を外部クエリに吸収する)と、この中間マテリアライズが排除され、エンジンが基盤テーブルに対して直接パラメータ化されたルックアップを構築できるようになります。書き換えパイプラインは、意味的に安全な場合に派生テーブルを積極的にインライン化し、インライン化がクエリの意味を変更する場合にはマテリアライズされた形式を予約します。
サポートされている結合タイプ。エンジンはINNER JOIN、LEFT OUTER JOIN、CROSS JOINをサポートしています。RIGHT JOINとFULL OUTER JOINは、ストリーミングコンテキストでの増分的な維持が両側でのマッチの不在を追跡する必要があるため、サポートされていません。これははるかに難しい問題です。
集計境界。GROUP BYと集計関数(COUNT、SUMなど)は、実行中の合計を維持するステートフルな演算子にコンパイルされます。エンジンは、各集計クエリが少なくとも1つの集計派生式を射影し、GROUP BYキーが明示的な列参照(位置番号やエイリアスではない)であることを要求します。
これらの制約は、PostgreSQLやMySQLがエラーなしで実行する構文的に有効なSQLクエリであっても、Readysetで直接コンパイルできない場合や、必要以上に非効率的なデータフローグラフにコンパイルされる可能性があることを意味します。クエリ書き換えパイプラインは、このギャップを埋めるために存在します。
書き換えパイプライン:SQLからデータフローへの橋渡し
ユーザーがクエリに対してCREATE CACHEを発行すると、Readysetはそのクエリを多段階の書き換えパイプラインに通し、任意のSQLをデータフローエンジンが期待する標準形式に変換します。パイプラインは3つのブロックに編成されています。
ブロック | 目的 | 例
A | 正規化 | 構文のデシグア化、スキーマの解決、列の修飾
SELECT * は SELECT t.id, t.name, ... になる
B | ディープリライト | サブクエリのデコレレーション、派生テーブルのフラット化、最適化
WHERE id IN (SELECT ...) は結合になる
C | クリーンアップ | 不要な句の削除、リテラルのパラメータ化
ORDER BY id LIMIT 10 は結果が単一行であることが証明された場合に削除される
各パスは意味を保持します。書き換えられたクエリは、すべての可能なデータに対して元のクエリと同じ結果を返します。パスは互いに積み重ねられます。ブロックAはSQLを標準形式に正規化し、ブロックBの変換が確実に機能するようにします。ブロックCはブロックBによって残されたアーティファクトをクリーンアップします。
ブロックA:SQLを標準化する
深い変換が行われる前に、クエリは予測可能な形状である必要があります。ブロックAがこれを処理します。
スキーマ解決は、Readysetがアップストリームデータベースからレプリケートした実際のスキーマメタデータにテーブル名と列名をバインドします。これは、主キー、一意制約、列の型を知る必要がある後続のパスに不可欠です。
スター展開はSELECT * を明示的な列リストに置き換えます。すべてのダウンストリームパスは、ワイルドカードではなく名前付き列を表示することを期待しています。
列修飾は、すべての列参照がテーブル名で修飾されていることを保証します(id は t.id になります)。これにより、複数のテーブルが同じ名前の列を持っている場合の曖昧さが防止されます。
USING句のデシグア化は、JOIN ... USING(id) を JOIN ... ON (a.id = b.id) に変換します。データフローエンジンはON述語で動作し、USING句では動作しません。
ブロックAの後、クエリは完全に解決され、修飾され、デシグア化され、後続の変換のためのクリーンな基盤となります。
ブロックB:重労働
ブロックBは実際の作業が行われる場所です。これらのパスは、データフローエンジンが処理できないSQL構造を、処理できる同等の構造に変換します。順序が重要です。各パスは次のパスの準備をします。
配列コンストラクタの書き換え
最初のブロックBパスであり、PostgreSQL固有です。PostgreSQLのARRAY(SELECT ...)コンストラクタは、サブクエリの行から配列値を生成します。これは、データフローエンジンが直接的な演算子を持たない、外部行ごとのスカラー集計の形状です。パイプラインは、各出現をLATERAL LEFT JOINに書き換えます。その本体は元のサブクエリをarray_agg(...)でラップし、外側にCOALESCE(..., ARRAY[])を適用して、空のサブクエリがNULLではなく空の配列を生成するようにします。コンストラクタ内のORDER BYとDISTINCTは、array_agg呼び出しにコピーされます(サブクエリにもLIMIT/Top-Kがある場合に正しさを保証するために必要)。最初に実行されることで、このパスは後続のパスが認識しないSQL構造を、それらが既に処理方法を知っているLATERAL結合に変換します。
-- 前: ARRAY(SELECT ...) コンストラクタ - 直接的なデータフロー演算子なし
SELECT u.name, ARRAY(SELECT p.title FROM posts p WHERE p.user_id = u.id) AS post_titles FROM