pg_clickhouse への投資を継続する中で、分析ワークロードに対するプッシュダウン対応範囲の向上は常に最優先事項であり、TPC-H ベンチマークスイート全体の完全なプッシュダウンを直近の指標として掲げてきました。6月の前回アップデート以降、多くの進展がありました。2025年12月の紹介記事以来あまり触れてこなかった TPC-H スコアボードでも前進が見られたため、まずはそこから見ていきます。v0.10.0 のリリースにより、完全にプッシュダウンされる TPC-H クエリは 22 件中 12 件から 16 件へと増加し、ベンチマーク完了まで残りわずか 6 件となりました。
あわせて、以下の改善も実施しました。
- 新しいプレーン C クライアントライブラリをベースにバイナリドライバーを再構築
- プッシュダウン対象となる関数と集計関数のカバレッジを 2 倍以上に拡大
- 以下で詳しく説明するいくつかの並行処理バグに対処し、バイナリドライバーを強化
スコアボード
新たに 3 つの TPC-H クエリが完全にプッシュダウンされるようになりました。これらはいずれも、クエリの構造上、pg_clickhouse が ClickHouse から各行を個別に取得した上でローカルでサブクエリを評価しなければならなかったため、以前は極めて非効率でした(全チャート)。
| クエリ | PostgreSQL | pg_clickhouse 0.3 | pg_clickhouse 0.10 | プッシュダウン |
|---|---|---|---|---|
| Q2 | 588 ms | 3,446 ms | 24 ms | ✔ |
| Q17 | 2107 ms | 32,709 ms | 37 ms | ✔ |
| Q22 | 270 ms | 1,415 ms | 45 ms | ✼ |
( ✔ = クエリ全体が単一の Foreign Scan )
( ✼ = プッシュダウンされるが、複数のリモートクエリとして実行。通常は外部スキャンに加え、1 つの InitPlan スキャン。)
最大の成果は Q17 です。これはパーツごとに l_quantity の平均を求める相関サブクエリで、スケールファクター 1 の 600 万行のラインアイテムに対して外部の行ごとに 1 回評価されていた頃は、32.7 秒かかっていました。完全にプッシュダウンされた現在では、37 ミリ秒で完了します。3 桁もの性能差であり、同じクエリに対するネイティブ PostgreSQL 自体の実行プラン(2.1 秒)を pg_clickhouse が大幅に上回る好例です。
プッシュダウンされていないクエリは、Q13、Q15、Q16、Q18、Q20、Q21 の 6 つが残っています。Q16 と Q18 は今後の方向性を示しています。pg_clickhouse は、これらに必要な SQL 構文(Q2 や Q17 と同様に、アンチ結合やセミ結合としてデパースされる IN および NOT IN)をすでにプッシュダウンできます。阻害要因となっているのは、PostgreSQL がこれらサブクエリを入力自体が結合であるアンチ結合やセミ結合へとフラット化することであり、現在のデパーサーは結合の両側にある結合ツリーの走査にまだ対応していません。Q15 と Q20 も同じ問題の派生パターンに直面しています。これがサブクエリプッシュダウンにおける次の大きな開発領域です。
サブクエリ対応の完成に向けて
12月の目玉機能は、外部の行ごとに ClickHouse との間でラウンドトリップが発生するネステッドループの代わりに、相関 EXISTS サブクエリ全体を単一の LEFT SEMI JOIN としてプッシュダウンするようプランナーを改良したことでした。これにより、プッシュダウン可能な TPC-H クエリは 22 件中 3 件から一気に 12 件へと増加しました。残る 10 件のクエリには共通する課題がありました。プランナーがサブクエリを結合に全くフォールドできず、SubPlan が残ってしまう点です。これは、クエリ全体の実行の一環として(通常は行ごとに 1 回)実行される個別クエリの完全な実行プランを表す実行プランノードです。このプッシュダウンはロードマップの 5 番目の項目でしたが、今回の最新リリース(0.10.0)で対応を完了しました(#289)。現在では、Postgres のサブクエリが 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;Foreign Scan on subplan_test.sales s
Output: s.sale_id, s.amount
Remote 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
SubPlan expr_1
-> Foreign Scan
Output: ((1.5 * avg(s2.amount)))
Relations: Aggregate on (sales s2)
Remote SQL: SELECT (1.5 * avg(amount)) FROM subplan_test.sales WHERE ((item_id = {p1:Int32}))
(8 rows)EXPLAIN には依然として SubPlan ノードが表示されていますが(これは相関関係を管理する PostgreSQL 側の処理にすぎません)、最上部の Remote SQL を見ると、サブクエリを含む比較全体が、ClickHouse に送信される単一のステートメントに含まれていることがわかります。これと同じ仕組みにより、pg_clickhouse は TPC-H Q2 全体をプッシュダウンできるようになりました(1 つの Foreign Scan と 1 つのリモートクエリ)。プランナーが変換の安全性を証明できる場合は常に、NOT IN にも LEFT ANTI JOIN(v0.1.0 のセミ結合の否定版)を介して同様の処理が適用されます。
この機能は、相関サブクエリの SQL 構文をサポートしていない ClickHouse 25.8 未満では動作しません。
pg_clickhouseはプラン生成時にサーバーのバージョンを確認し、サポートされていない構文に対して常にそうしているように、古いサーバーではローカル評価へとフォールバックします。
NOT IN の正しい実装
SQL のプッシュダウン自体は容易な部分でした。より難しかったのは、PostgreSQL と同一の結果を確実に計算できるようにすることであり(#315、#317)、これ自体が複雑を極める課題でした。ClickHouse の IN は二値論理で動作するのに対し、PostgreSQL の IN は三値論理で動作します。つまり、PostgreSQL では x NOT IN (1, NULL) は FALSE(x=1 の場合)または NULL になり得ますが、決して TRUE にはなりません。これを単純にプッシュダウンすると、比較に NULL が含まれる箇所で結果が暗黙的に反転し、PostgreSQL なら除外するはずの行を WHERE NOT IN が返したり、GROUP BY が NULL グループを FALSE に統合してしまったりする可能性があります。これらは通常のテストでは表面化しにくく、それこそがプッシュダウン拡張機能において危険な理由です。プランは正しく見えても、出力結果が微妙に誤ったものになってしまいます。
v0.10 の修正では、各式の結果がどのように利用されるかを追跡し、結果の一貫性を保つためにクエリ側でどれだけのガード処理が必要かを把握できるようにしました。この問題はまさにメンガーのスポンジのような様相を呈していました。
- フィルター条件では
NULLをFALSEと同様に扱っても問題ないため、NOTの外側の条件では ClickHouse の挙動をそのまま利用できます。 - 値の位置や否定演算では、結果に正しい Postgres の値を注入するために、NULL 値の追加チェックが必要になります。
- オペランドが
NULLになり得ないことを pg_clickhouse が証明できる場合(非 NULL 定数までトレースできる、外部結合によって再び NULL 化されていないNOT NULL制約付きカラムである、これらに対する非 NULL 演算である、など)、Postgres の挙動を注入するガードの追加を省略できます。
したがって、ガードを適用して Postgres の挙動を実装したクエリは以下のようになります。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM tnull WHERE xn NOT IN (1, NULL) ORDER BY id;Foreign Scan on in_null_test.tnull
Output: id
Remote 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
(3 rows)一方で、NULL を含まない一般的なケースでは、シンプルなネイティブの IN として送信できます。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM tnull WHERE xn NOT IN (1, 500) ORDER BY id;Foreign Scan on in_null_test.tnull
Output: id
Remote SQL: SELECT id FROM in_null_test.tnull WHERE ((xn NOT IN (1,500))) ORDER BY id ASC NULLS LAST
(3 rows)その後の対応(#317)では、この挙動をスカラ形式と配列形式を問わず、IN ファミリーの演算子全体(IN、NOT IN、= ANY、= ALL、<> ANY、<> ALL)へと一般化しました。これにより、非 NULL 性が証明できない場合でもローカル評価にフォールバックすることなく、無条件でプッシュダウンできるようになりました。
さらに、<> ANY(array) が実際には <> ALL を計算していた小さなバグも存在していたため、関連箇所の改修に合わせて修正しました。
これらはすべて、ClickHouse の IN が前述の二値論理どおりに動作するという前提に基づいています。サーバーレベルの設定(transform_null_in)によってこの挙動は変化します。そのため、デフォルトの pg_clickhouse.session_settings に transform_null_in 0 を追加し、ClickHouse サーバーのプロファイルによって上記ガード処理の前提が暗黙的に崩されないようにしました。
ClickHouse Managed Postgres を今すぐ始める
ClickHouse Managed Postgres を自社のデータで試してみませんか?ClickHouse Cloud はわずか数分で利用開始でき、300 ドル分の無料クレジットも進呈されます。
サインアッププッシュダウン対象の拡大
結合やサブクエリへの対応と並行して、プッシュダウンされる個別の関数、演算子、集計関数のリストも大幅に拡大しました。ここですべてを網羅しきれないほど多岐にわたるため(詳細は CHANGELOG を参照してください)、対象範囲が広がった代表例を以下に紹介します。
-
正規表現: これに関する多くのアップデートは以前のブログ投稿で取り上げましたので、ぜひご覧ください。
-
集計関数:
-
パーティション単位の集計 (#298): 分析処理の多いパーティションを ClickHouse に移行し、トランザクション処理の多いパーティションを Postgres に残している場合に便利です。
- 外部パーティションの計算: ローカルパーティションと外部パーティションが混在するパーティションテーブルを集計するクエリで、外部パーティションの行を取得する代わりに、その分の計算を ClickHouse 側で実行するようになりました。
- これは整数に対する
count、sum、min,max、avgなどの分解可能な集計関数が対象です。 enable_partitionwise_aggregateを有効にする必要があります。
- これは整数に対する
- 外部パーティションの計算: ローカルパーティションと外部パーティションが混在するパーティションテーブルを集計するクエリで、外部パーティションの行を取得する代わりに、その分の計算を ClickHouse 側で実行するようになりました。
-
その他の機能:
- フォーマットとエンコーディング:
encode(bytea, 'hex'|'base64'|'base64url')(#302)。 - 文字列: 3 引数の
ltrim/rtrim/btrim(#307)。 - コスト推定: プランナが ClickHouse 上の MIN/MAX に対してより低コストな選択肢を選び続けるよう、一部のコスト関数を修正しました (#310)。
- 日付と時刻:
- 期間(Interval)演算を
date/timestampのオペランドおよび減算に拡張 (#301)。 - セッションのタイムゾーンおよびミリ秒・マイクロ秒精度のために
CURRENT_*/now()/clock_timestamp()ファミリを再調整。
- 期間(Interval)演算を
- フォーマットとエンコーディング:
これらすべての基盤として、アーキテクチャ上の重要な変更があります。それは、組み込み関数のプッシュダウンがオプトイン方式になったことです (#245)。初期の頃は、ClickHouse の関数と名前が一致する Postgres の組み込み関数はデフォルトですべてプッシュダウンされていました。しかしこれでは、シグネチャや動作の違いによって結果が暗黙のうちに変わってしまう恐れがありました。この変更のきっかけとなった具体的な事例が三角関数(asin/acos/atanh/acosh)です。Postgres では定義域外の値でこれらを呼び出すとエラーが発生しますが、ClickHouse は NaN を返します。v0.3.0 以降、明示的にマッピングされた関数のみがプッシュダウンされるようになりました。対応リストの拡充スピードは落ちますが、Postgres のセマンティクスを維持するという点で信頼できるリストになっています。
ドライバーのアップデート
紹介記事では、ネイティブプロトコルでのアクセスのために clickhouse-cpp を採用することで、従来の clickhouse_fdw の系譜を刷新したと説明しました。しかし、それはすでに過去の話です。v0.3.1 では、git サブモジュールとして取り込まれた新しい C クライアントである ClickHouse/clickhouse-c に全面的に置き換えました (#254)。この切り替えは単なる依存関係の更新にとどまりません。C++ の例外処理と PostgreSQL 独自の setjmp/longjmp ベースのエラー処理が混在していたことがクラッシュの原因となっており、clickhouse-cpp のモノリシックな結果バッファリングにより、結果サイズに比例してメモリ使用量が増加していました。対照的に、clickhouse-c は結果をブロック単位でストリーミングし、取り込まれたライブラリのビルド時間とサイズを 75% 以上削減しました。
最初の clickhouse-c への移行後も、統合を進めました。HTTP ドライバーも ClickHouse の Native フォーマットを扱えるようになり、従来の TSV ベースのパスに代わって、バイナリドライバーが既に使用していたのと同じエンコードとデコードを利用するようになりました (#328)。この変更に伴い、HTTP ドライバーの以前のストリーミング作業で導入されたバッチ処理オプションである fetch_size は不要となりました。ネイティブデコーダーはどちらにしても curl のチャンクを一度に 1 つずつストリーミングするため、その設定で構成する余地がなくなり、非推奨となりました。書き込み側では、バイナリドライバーがバッチ全体をメモリに保持する代わりに、64 MiB を超えたバッファ済みの INSERT/COPY FROM データをフラッシュするようになりました (#303)。両方のドライバーで明示的な compression(none/lz4/zstd)(#268) と TLS 制御(secure = on/off/auto、min_tls_version)(#272) が追加され、ホスト名とポートから TLS を推測するヒューリスティックな処理が置き換えられました。型の対応範囲も広がり、両方のドライバーで読み書き両方の多次元配列をサポートし (#233)、バイナリプロトコル経由での Array(Nullable(T)) の挿入にも対応しました (#316)。
両ドライバーが 1 つのバイナリエンコードパスを共有するようになったことで、この開発サイクルでいくつかの信頼性向上修正も可能になりました。バイナリドライバーでの並行外部スキャン(例: 相関サブクエリや 2 つの外部テーブルに対するネステッドループ結合)は、1 つの共有接続で衝突してクラッシュしていましたが、並行スキャンごとに専用の接続が割り当てられるようになりました (#296)。これにより、再スキャンされた外部スキャンのバッチメモリコンテキストにおける関連の use-after-free も解消されました。また、クエリを実行するユーザーを選択する際に無効なリレーション OID を選択してしまい、特定のクエリ構造の実行が失敗していた問題も修正しました (#319)。
さらに詳細な点として、HTTP 経由でタイムスタンプを挿入する際の秒未満の精度の低下も解消しました (#300)。さらに仕上げとして、コードベース全体に対するより広範な静的解析パスによって、潜在的なバグがいくつか発見され修正されました (#313)。これらは実環境では顕在化していませんでしたが、問題が起きる前に対処できました。
新しい機能領域
自動的にプッシュダウンされる機能だけでなく、ユーザーが「実行できること」を広げる追加もいくつか行われました。clickhouse_query(server, sql) は構成済みのサーバーに対して任意のクエリを実行し、結果を指定したカラム定義リストの型に変換します。これにバイナリドライバーのサポートが追加されました (#309)。このカラム定義リストの指定は必須ですが(Postgres は行のフェッチを開始する前に行の形状を把握しておく必要があります)、これは同時に、行を返さず宣言すべき形状もない CREATE TABLE のような処理を clickhouse_query() で実行できないことを意味します。そのために追加されたのが、新しい対となる機能である clickhouse_perform(server, sql) です。これは行を取得するためではなく、効果を発生させるために実行するステートメント向けに、SELECT ではなく CALL で呼び出すプロシージャです (#329)。さらに、主には条件分岐ロジックの内部用途向けとして、clickhouse_server_version(server) が接続先サーバーのバージョンを返します (#293)。
これまでこの用途に clickhouse_raw_query() を使用していた場合、この関数は非推奨となり、次のリリースで削除される予定です (#329)。代わりに clickhouse_query() または CALL clickhouse_perform() に移行してください。どちらも、その場限りの未加工な接続文字列ではなく、独自の接続処理とドライバー選択を備えた構成済みの外部サーバーを経由します。
既存の機能領域
過去のリリースに含まれるこれらの機能の多くについては他の場所でお知らせしていましたが、ここでの記載が漏れていたため、改めて整理してお伝えします。
-
JSON:
-
配列:
- 新しい関数と演算子: 10 以上の関数がプッシュダウンされます。
array_catappendremoveto_stringlength@>/<@/&&に対するhasAll/hasAnyarraySlice()としてのスライス構文 (arr[L:U])- その他多数...
- 新しい関数と演算子: 10 以上の関数がプッシュダウンされます。
-
集計関数:
-
その他の機能:
今後の課題
残る 6 つの TPC-H クエリ以外にも、ロードマップ当初の課題が多く残されています。未対応の PostgreSQL 関数、軽量な DELETE/UPDATE、そして UNION のプッシュダウンなどです。Q15/16/18/20 を妨げている両側結合ツリーの制限は最も影響の大きい部分であり、次のリリースまでに 6 つすべてを解消できなければ、このシリーズの次回の記事で自然と取り上げることになるでしょう。



