プログラミング
pg_clickhouse v0.10: サブクエリプッシュダウンとTPC-Hクエリの1000倍高速化
pg_clickhouse v0.10: Subquery pushdown and 1000x faster TPC-H queries (clickhouse.com)
要約
pg_clickhouse v0.10.0のリリースにより、TPC-Hベンチマークにおけるサブクエリプッシュダウンの対応範囲が大幅に拡大しました。特に、相関サブクエリのプッシュダウンが改善され、一部のクエリではネイティブPostgreSQLを凌駕するパフォーマンスを発揮します。また、NOT IN演算子などの3値論理の扱いや、関数・集計処理のプッシュダウンも強化されています。
全文翻訳
pg_clickhouseへの投資を継続し、分析ワークロードのプッシュダウンカバレッジの改善が最優先事項であり、TPC-Hベンチマークスイート全体での完全なプッシュダウンを直近の指標としてきました。12月の最初の投稿以来、TPC-Hスコアボードに関してあまり触れていませんでしたが、進捗は大きく、そこから始めます。
v0.10.0のリリースにより、スコアボードはTPC-Hクエリ22件中12件から16件が完全にプッシュダウンされるようになり、残りはあと6件となりました。その過程で、バイナリドライバを新しいプレーンCクライアントライブラリ上で再構築し、プッシュダウンされる関数と集計処理の表面積を2倍以上に増やし、以下に詳述するいくつかの競合状態バグに対してバイナリドライバを強化しました。
スコアボード
さらに3件のTPC-Hクエリが完全にプッシュダウンされるようになりました。これら3件は、クエリの形状により、pg_clickhouseがClickHouseから各行を個別に取得し、ローカルでサブクエリを評価する必要があったため、以前は非常に非効率的でした(完全なチャートを参照)。
Query
PostgreSQL
pg_clickhouse 0.3
pg_clickhouse 0.10
Pushdown
Q2
588 ms
3,446 ms
24 ms
✔
Q17
2107 ms
32,709 ms
37 ms
✔
Q22
270 ms
1,415 ms
45 ms
✼
( ✔ = クエリ全体が単一の外部スキャン )
( ✼ = プッシュダウンされたが、複数のリモートクエリとして。通常は外部スキャンと1つのInitPlanスキャンです。)
Q17は特に注目に値します。これは相関サブクエリで、スケールファクター1で600万件の明細行に対して1行あたり平均l_quantityを計算するもので、以前は32.7秒かかっていました。完全にプッシュダウンされると、37ミリ秒になります。これは3桁の違いであり、pg_clickhouseが同じクエリに対するネイティブPostgreSQL自身のプラン(2.1秒)を上回る明確なケースを示しています。プッシュダウンされていないクエリは6件残っています:Q13、Q15、Q16、Q18、Q20、Q21です。Q16とQ18は今後の方向性を示しています。pg_clickhouseは既にそれらが必要とするSQL形状(Q2やQ17のように、INとNOT INがアンチ/セミ結合としてデパースされる)をプッシュダウンしていますが、それらをブロックしているのは、PostgreSQLがそれらのサブクエリを結合の入力自体が結合であるアンチ/セミ結合にフラット化し、デパーサーがまだ結合の両側で結合ツリーをウォークしないことです。Q15とQ20は同じ問題のバリアントにヒットします。これがサブクエリプッシュダウンの次のまとまった部分です。
サブクエリのストーリーを完成させる
12月のヘッドライン機能は、プランナーに相関EXISTSサブクエリ全体を、ネストされたループと外部行ごとのClickHouseラウンドトリップの代わりに、単一のLEFT SEMI JOINとしてプッシュダウンすることを教えることでした。これにより、TPC-Hクエリ22件中3件から12件に大幅に進歩しました。残りの10件のクエリは共通の問題を抱えていました。プランナーはサブクエリを結合に折りたたむことができず、SubPlanを残していました。これは、クエリプランの一部であり、通常は行ごとに実行される完全なクエリプランを記述するものです。それをプッシュダウンすることは、ロードマップの5番目の項目であり、最新リリース(0.10.0)で完了しました(#289)。これで、PostgreSQLのサブクエリはClickHouseのサブクエリになります。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT s.sale_id, s.amount FROM sales s
WHERE s.amount > (SELECT 1.5 * avg(s2.amount) FROM sales s2
WHERE s2.item_id = s.item_id)
ORDER BY s.sale_id;
Copy command
1Foreign Scan on subplan_test.sales s
2Output: s.sale_id, s.amount
3Remote SQL: SELECT sale_id, amount FROM subplan_test.sales r1 WHERE ((r1.amount > (SELECT (1.5 * avg(q1_1.amount)) FROM subplan_test.sales q1_1 WHERE ((q1_1.item_id = (r1.item_id)))))) ORDER BY r1.sale_id ASC NULLS LAST
4SubPlan expr_1
5-> Foreign Scan
6Output: ((1.5 * avg(s2.amount)))
7Relations: Aggregate on (sales s2)
8Remote SQL: SELECT (1.5 * avg(amount)) FROM subplan_test.sales WHERE ((item_id = {p1:Int32}))
9(8 rows)
Copy command
EXPLAINはまだSubPlanノードを示していますが(これは相関関係のPostgreSQLのブックキーピングにすぎません)、トップのRemote SQLには、ClickHouseに送信する単一のステートメント全体にサブクエリが含まれていることがわかります。同じメカニズムにより、pg_clickhouseはTPC-H Q2全体をプッシュダウンできます。1つの外部スキャンと1つのリモートクエリです。NOT INは、プランナーが変換が安全であると証明できる場合はいつでも、LEFT ANTI JOIN(v0.1.0のセミ結合の否定形)を介して同じ処理を受け取ります。これらは、ClickHouse 25.8未満では機能しません。これは相関サブクエリのSQL形状をサポートしていないためです。pg_clickhouseはプラン時にサーバーバージョンをチェックし、サポートされていない形状に対して常にそうするように、古いサーバーではローカル評価にフォールバックします。
NOT IN、正しく実装
SQLのプッシュダウンは簡単な部分でした。より難しい部分は、PostgreSQLと同じ結果を計算することを確実にすることでした(#315、#317)。これはそれ自体が深い問題でした。ClickHouseのINは2値論理で動作し、PostgreSQLは3値論理で動作します。これは、x NOT IN (1, NULL) がPostgreSQLではFALSE(x=1)またはNULLになり得ますが、TRUEにはなり得ないことを意味します。単純にプッシュダウンすると、NULLが比較に関与する場合、これらの式は結果を静かに反転させる可能性があり、WHERE NOT INがPostgreSQLがフィルタリングする行を返したり、GROUP BYがNULLグループをFALSEにマージしたりします。これらは通常のテストでは現れず、プッシュダウン拡張機能では危険な理由がそこにあります。プランは正しく見え、出力は微妙に間違っています。v0.10の修正は、各式の結果がどのように消費されるかを追跡するため、結果の一貫性を保つためにクエリがどの程度注意を払う必要があるかがわかります。ここで、この深い問題はメンガーのスポンジのような様相を呈しました。フィルタ条件はNULLをFALSEとして無料で扱うことができるため、NOT以外の条件ではClickHouseの動作は問題ありません。値の位置または否定は、Postgres値を結果に注入するためにNULL値の追加チェックが必要です。pg_clickhouseがオペランドがNULLになり得ないことを証明できる場合(非NULL定数、または外部結合によってNULL化されていないNOT NULL制約を持つ列、あるいはそれらの上のNULLを許容しない操作にトレースすることによって)、NULLを注入するガードを追加するのをスキップできます。そのため、ガードとPostgresの動作が実装されたクエリは次のようになります。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM tnull WHERE xn NOT IN (1, NULL) ORDER BY id;
Copy command
1Foreign Scan on in_null_test.tnull
2Output: id
3Remote SQL: SELECT id FROM in_null_test.tnull WHERE ((CASE WHEN xn IS NULL AND notEmpty([1,NULL]) THEN NULL WHEN countEqual([1,NULL], xn) > 0 THEN false WHEN countEqual([1,NULL], NULL) > 0 THEN NULL ELSE true END)) ORDER BY id ASC NULLS LAST
4(3 rows)
Copy command
一方、一般的なNULLフリーのケースは、通常のネイティブINとして送信できます。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM tnull WHERE xn NOT IN (1, 500) ORDER BY id;
Copy command
1Foreign Scan on in_null_test.tnull
2Output: id
3Remote SQL: SELECT id FROM in_null_test.tnull WHERE ((xn NOT IN (1,500))) ORDER BY id ASC NULLS LAST
4(3 rows)
Copy command
フォローアップ(#317)では、この動作をIN演算子のファミリー全体(IN、NOT IN、= ANY、= ALL、<> ANY、<> ALL)、スカラー形式と配列形式の両方に一般化し、非NULL可能性が証明できない場合にローカル評価にフォールバックするのではなく、無条件にプッシュダウンできるようにしました。さらに、<> ANY(array)が実際には<> ALLを計算するという小さなバグがあり、それを修正しました。これらすべては、ClickHouseのINが実際に説明されている2値の方法で動作するという仮定に基づいています。サーバーレベルの設定(transform_null_in)でそれを変更できます。そのため、デフォルトのpg_clickhouse.session_settingsにtransform_null_in 0を追加し、ClickHouseサーバープロファイルが説明したガードを静かに無効にできないようにしました。
ClickHouse Managed Postgresで今すぐ始めましょう
ClickHouse Managed Postgresがデータでどのように機能するか見てみませんか?数分でClickHouse Cloudを開始し、300ドルの無料クレジットを受け取ってください。
サインアップ
プッシュダウンされるものの範囲を広げる
結合とサブクエリの作業と並行して、個々の関数、演算子、および集計処理のプッシュダウンリストが大幅に増加しました。ここにすべてをリストするには多すぎます(詳細についてはCHANGELOGを参照)。しかし、どこから来たかのサンプルを以下に示します。