Skip to content

pg_clickhouse v0.10.0 の新機能: サブクエリ、TPC-H の高速化、C ドライバー、集約関数の拡張

josh ventura
2026年8月11日 · 21分で読む

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 から各行を個別に取得した上でローカルでサブクエリを評価しなければならなかったため、以前は極めて非効率でした(全チャート)。

クエリPostgreSQLpg_clickhouse 0.3pg_clickhouse 0.10プッシュダウン
Q2588 ms3,446 ms24 ms
Q172107 ms32,709 ms37 ms
Q22270 ms1,415 ms45 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 BYNULL グループを FALSE に統合してしまったりする可能性があります。これらは通常のテストでは表面化しにくく、それこそがプッシュダウン拡張機能において危険な理由です。プランは正しく見えても、出力結果が微妙に誤ったものになってしまいます。

v0.10 の修正では、各式の結果がどのように利用されるかを追跡し、結果の一貫性を保つためにクエリ側でどれだけのガード処理が必要かを把握できるようにしました。この問題はまさにメンガーのスポンジのような様相を呈していました。

  • フィルター条件では NULLFALSE と同様に扱っても問題ないため、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 ファミリーの演算子全体(INNOT IN= ANY= ALL<> ANY<> ALL)へと一般化しました。これにより、非 NULL 性が証明できない場合でもローカル評価にフォールバックすることなく、無条件でプッシュダウンできるようになりました。

さらに、<> ANY(array) が実際には <> ALL を計算していた小さなバグも存在していたため、関連箇所の改修に合わせて修正しました。

これらはすべて、ClickHouse の IN が前述の二値論理どおりに動作するという前提に基づいています。サーバーレベルの設定(transform_null_in)によってこの挙動は変化します。そのため、デフォルトの pg_clickhouse.session_settingstransform_null_in 0 を追加し、ClickHouse サーバーのプロファイルによって上記ガード処理の前提が暗黙的に崩されないようにしました。

ClickHouse Managed Postgres を今すぐ始める

ClickHouse Managed Postgres を自社のデータで試してみませんか?ClickHouse Cloud はわずか数分で利用開始でき、300 ドル分の無料クレジットも進呈されます。

サインアップ

プッシュダウン対象の拡大

結合やサブクエリへの対応と並行して、プッシュダウンされる個別の関数、演算子、集計関数のリストも大幅に拡大しました。ここですべてを網羅しきれないほど多岐にわたるため(詳細は CHANGELOG を参照してください)、対象範囲が広がった代表例を以下に紹介します。

  • 正規表現: これに関する多くのアップデートは以前のブログ投稿で取り上げましたので、ぜひご覧ください。

  • 集計関数:

    • 統計集計関数 (#290)
      • corrcovar_pop/samp
      • stddev_pop/samp
      • var_pop/samp
      • any_value
    • 順序集合集計関数 (#291): ClickHouse のパラメータ付き形式にマッピングされます。
      • percentile_cont/discquantile(s)/quantileExactLow
  • パーティション単位の集計 (#298): 分析処理の多いパーティションを ClickHouse に移行し、トランザクション処理の多いパーティションを Postgres に残している場合に便利です。

    • 外部パーティションの計算: ローカルパーティションと外部パーティションが混在するパーティションテーブルを集計するクエリで、外部パーティションの行を取得する代わりに、その分の計算を ClickHouse 側で実行するようになりました。
      • これは整数に対する countsummin, maxavg などの分解可能な集計関数が対象です。
      • enable_partitionwise_aggregate を有効にする必要があります。
  • その他の機能:

    • フォーマットとエンコーディング: encode(bytea, 'hex'|'base64'|'base64url') (#302)。
    • 文字列: 3 引数の ltrim/rtrim/btrim (#307)。
    • コスト推定: プランナが ClickHouse 上の MIN/MAX に対してより低コストな選択肢を選び続けるよう、一部のコスト関数を修正しました (#310)。
    • 日付と時刻:
      • 期間(Interval)演算を date/timestamp のオペランドおよび減算に拡張 (#301)。
      • セッションのタイムゾーンおよびミリ秒・マイクロ秒精度のために CURRENT_*/now()/clock_timestamp() ファミリを再調整。

これらすべての基盤として、アーキテクチャ上の重要な変更があります。それは、組み込み関数のプッシュダウンがオプトイン方式になったことです (#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:

    • 演算子と関数: ->/->> および jsonb_extract_path[_text]() は、ClickHouse のサブカラム構文にマッピングされます (#169#176)。
    • 型: ClickHouse ネイティブの JSON 型は、同様の演算子サポートとともに Postgres の json にマッピングされます。
  • 配列:

    • 新しい関数と演算子: 10 以上の関数がプッシュダウンされます。
      • array_cat
      • append
      • remove
      • to_string
      • length
      • @>/<@/&& に対する hasAll/hasAny
      • arraySlice() としてのスライス構文 (arr[L:U])
      • その他多数...
  • 集計関数:

    • ウィンドウ関数 (#175): 以下を含む一連の機能が完全にプッシュダウンされます。
      • ROW_NUMBER
      • RANK
      • LEAD/LAG
      • NTILE
    • ブール値と文字列 (#184): bool_andbool_orstring_agg
  • その他の機能:

    • フォーマット文字列のバリデーションを伴う to_char() (#244)
    • split_part() (#206)
    • fuzzystrmatchsoundex()/levenshtein() (#210)。

今後の課題

残る 6 つの TPC-H クエリ以外にも、ロードマップ当初の課題が多く残されています。未対応の PostgreSQL 関数、軽量な DELETE/UPDATE、そして UNION のプッシュダウンなどです。Q15/16/18/20 を妨げている両側結合ツリーの制限は最も影響の大きい部分であり、次のリリースまでに 6 つすべてを解消できなければ、このシリーズの次回の記事で自然と取り上げることになるでしょう。


この記事をシェア

  • Y Combinator icon
  • X icon
  • Bluesky icon
  • Facebook icon
  • LinkedIn icon

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!

Aditya Chidurala, José Muñoz and Alex Francoeur · Sep 16, 2026
Amy Chen and Jan Mensch · Sep 15, 2026

Follow us

XBlueskySlackGithubTelegramMeetupRSS