1. はじめに
ClickHouse が公開ベータとして出している Postgres managed by ClickHouse(以下 Managed Postgres)には、Postgres の変更を ClickHouse のネイティブテーブルへ継続的に複製する CDC(Change Data Capture、変更データキャプチャ)の仕組みがあります。実体は ClickHouse の ClickPipes(マネージドな取り込みサービス)の Postgres CDC コネクタです。
以前、pg_clickhouse(FDW)のプッシュダウンを別記事(TPC-H 22 本をプッシュダウンで実測)で測りました。そのときも CDC は使っていましたが、役割は「初回ロードの手段」止まりで、増分レプリケーションの速さ・遅延・障害耐性そのものは測っていません。本記事はそこを正面から実測します。
公式ブログには、ロードマップとして次の記述があります。
sub-second replication latency, replication of ongoing transactions using logical replication v2
(拙訳: サブ秒のレプリケーション遅延、論理レプリケーション v2 による進行中トランザクションの複製)
(Postgres managed by ClickHouse | ClickHouse Blog)
「サブ秒のレプリケーション遅延」が今後の目標として挙げられている、ということは、裏を返せば現時点ではサブ秒ではないと読めます。では、いま実際の CDC の遅延は何秒なのか。そして「速くしようとすると何が犠牲になるのか」。本記事で実測します。
1.1. 今回の検証ゴール
| # | 検証項目 | 確認できれば OK の条件 |
|---|---|---|
| 1 | 増分 CDC の レプリケーション遅延(ソースの変更が ClickHouse に反映されるまでの時間)を実測し、Sync interval を変えたときのトレードオフと 重複排除(dedup)の遅れ を切り分ける | 既定 60 秒の遅延分布を採取し、interval を縮めると遅延↓ の代わりに何が悪化するかを数値化できる |
| 2 | restart(再起動)を跨いだときの 欠損 / 重複 と自動再開 | 連番を流しながら再起動し、ClickHouse 側で抜け・重複・確定行数を突き合わせられる |
1.2. 結論(先出し)
- 既定(Sync interval = 60 秒)のレプリケーション遅延は、中央値で約 36 秒・最大で約 60.7 秒でした。同期は 60 秒ごとに走るため、更新が入ったタイミングによって待ち時間が「ほぼ即時〜約 60 秒」までばらつくノコギリ波になります。公式ロードマップの「サブ秒」は、現時点では未提供と考えられます
- Sync interval を縮めると遅延は下がりますが、ClickHouse 側のマージ(後述)が急増します。1 秒に設定すると遅延は中央値約 1 秒まで下がる一方、マージ回数が毎分 50 件規模まで跳ね上がりました(60 秒ではゼロ)。「速さ」は ClickHouse のマージ負荷とのトレードオフです
- 遅延は 2 層あります。①行が ClickHouse に届くまで(レプリケーション遅延、速い)と、②届いた後に重複排除(dedup)が終わって最新行に確定するまで(遅い)です。dedup は 180 秒たっても完了しないケースがあり、正しい最新行を読むには
FINAL(後述)が必須でした - restart には強いです。再起動を跨いでも 欠損ゼロ・重複ゼロで、レプリケーションスロットとパイプが自動再開しました。ただし実ダウンタイムは約 32 秒で、データが乗った状態では「一瞬で再起動」ではありませんでした
2. 検証環境
| 項目 | 値 |
|---|---|
| ソース | ClickHouse Managed Postgres(ベータ, Tokyo / ap-northeast-1) |
| Postgres バージョン | 18 |
| ソース構成 | 8 vCPU / 64 GiB / NVMe、スタンバイ 0(HA なし) |
| ターゲット | ClickHouse Cloud サービス chtest(minimal, 1 replica, 12 GiB, Idling 15 分) |
| パイプ | ClickPipes Postgres CDC pipetest
|
| 複製対象 |
cdc_probe(計測用の小テーブル)/ pgbench_accounts(20,000,000 行, pgbench SF200) |
| クライアント | ローカル環境(WSL Ubuntu 24.04)+ Python(psycopg / clickhouse-connect) |
2.1. ClickPipe の設定(作成時の既定値)
今回のパイプは、作成ウィザードの既定値をほぼそのまま使っています。本記事の数値はこの設定が前提です。
| 設定 | 値 |
|---|---|
| Replication method | Initial load + CDC(初回ロード+増分) |
| Sync interval | 60 秒(既定。3 章で変更しながら測定) |
| Pull batch size | 100,000 |
| Parallel threads for initial load | 4 |
| Snapshot rows per partition | 100,000 |
| Snapshot tables in parallel | 1 |
| Remove deleted rows during merges | OFF(既定) |
| 宛先データベース |
default(スキーマ名プレフィックス OFF) |
3. ClickPipes の CDC の仕組み
本記事の計測を読むうえで、ClickPipes の CDC が「どう動いているか」を先に確認します。
3.1. Sync interval ごとにまとめて取り込む
ClickPipes は Postgres の論理レプリケーション(パブリケーション+レプリケーションスロット)を土台にしつつ、ClickHouse へは Sync interval ごとに、たまった変更をまとめて取り込む方式です。
公式 Docs には次のように書かれています(Controlling the syncing of a Postgres ClickPipe)。
- Sync interval は「ClickPipe がソース DB から変更レコードを取得しに行く間隔」で、既定 60 秒・推奨下限 10 秒
- この間隔には「ClickHouse へ書き込む時間」は含まれない
なぜ 1 行ずつ流さずまとめるかというと、ClickHouse が列指向で大きなバッチでの INSERT を前提にしているためです。小刻みに入れると小さなパーツ(part = ClickHouse がデータを格納する物理的なかたまり)が大量にでき、後述のマージ負荷や "too many parts" エラーの原因になります。推奨下限が 10 秒に設定されているのも、これが理由と考えられます。
用語: レプリケーション遅延(replication lag)
公式は「ソーステーブルで変更が起きてから、ターゲットテーブルにその変更が反映されるまでの時間」を latency(= replication lag)と定義しています(ClickPipes for PostgreSQL CDC | Docs)。本記事の「遅延」もこの意味で、Postgres でコミットしてから ClickHouse に行が現れるまでを指します。
3.2. ReplacingMergeTree で「後から重複排除」する
ClickPipes は宛先テーブルに ReplacingMergeTree というテーブルエンジンを使います。
- INSERT / UPDATE は、
_peerdb_version列の値が異なる新しい行として追加されます(上書きはしない) - DELETE は、
_peerdb_is_deleted列でマークした行として追加されます - ClickHouse は裏で、複数のパーツを定期的に 1 つに統合する処理(マージ)を行います。ReplacingMergeTree は、このマージのときに 重複排除(dedup, deduplication) を行い、主キーごとに最も新しい
_peerdb_versionの行だけを残します
ここで重要なのは、マージは非同期で、いつ起きるかが保証されない点です。公式 FAQ にも「dedup は非同期なので、一時的に重複が見えることがある」と明記されています(ClickPipes for Postgres FAQ)。そのため、
- ① 行が ClickHouse に届くまで(レプリケーション遅延)
- ② 届いた後、マージで重複が消えて最新行に確定するまで(dedup の遅れ)
は別物です。②はレプリケーション遅延とは異なるもので、ClickHouse 側に決まった呼称が見当たらないため、本記事では「dedup 完了までの時間」と呼びます。なお、FINAL(クエリ時に重複排除済みの結果を返す指定)を使えば、マージを待たずに最新行を読めます。
4. 計測方法
本記事の計測手法は筆者が独自に組んだもので、ClickPipes 公式のメトリクスとは別物です。絶対値の精度を保証するものではなく、「Sync interval を変えると遅延やマージがどう動くか」という傾向を見る目的で参照してください。
4.1. 遅延は「同じ PC 上で測った経過時間」で見る
レプリケーション遅延は次のように測りました。
- Postgres の
cdc_probeに 1 行 INSERT してコミットした直後の時刻を t0 とする - その行が ClickHouse に現れた瞬間(高頻度ポーリングで検出)の時刻を t1 とする
- 遅延 = t1 − t0(t0・t1 とも同じ PC の時計で測った経過時間)
Postgres サーバ側の時刻と ClickHouse 観測時刻を直接引き算すると、サーバと計測 PC の時計のズレが遅延に混ざってしまいます。それを避けるため、t0・t1 はどちらも計測 PC 側の時計で取り、その差を取っています。
計測の落とし穴(同期サイクルとの位相ずれ): 当初、「1 行 INSERT → 着信を待つ → 次を INSERT」という逐次方式で測ったところ、3 回とも遅延が 60.6 秒でほぼ一定になりました。これは毎回「同期が終わった直後に INSERT」する形になり、常に次の同期までまるまる 1 サイクル待つ最悪値だけを測っていたためです。
そこで、固定レートで INSERT し続ける処理と、着信をポーリングする処理を分離し、INSERT を同期ウィンドウ全体に散らすことで、本来の遅延分布が取れるようにしました。
4.2. トレードオフは ClickHouse のシステムテーブルで見る
Sync interval を変えながら、system.parts(アクティブなパーツ数・行数)、system.merges(進行中のマージ)、system.part_log(直近のマージ回数)をサンプリングし、「interval を縮めると ClickHouse 側で何が起きるか」を採取しました。
4.3. restart は連番の突き合わせで見る
連番を継続 INSERT しながらコンソールから Restart を実行し、接続断(DOWN)と再接続(実ダウンタイム)を記録します。再起動後、クライアントがコミットに成功した id の集合を ClickHouse と突き合わせ、欠損(committed なのに ClickHouse に無い)と重複(同一 id が複数版) を数えました。
5. 計測結果
5.1. 既定(60 秒)の遅延はノコギリ波
Sync interval = 60 秒で、低レートに INSERT を散らしてレプリケーション遅延を 70 サンプル取得しました。
| 指標 | 値 |
|---|---|
| 最小 | 1.09 秒 |
| 中央値 | 35.9 秒 |
| p95 | 59.2 秒 |
| 最大 | 60.7 秒 |
| 標準偏差 | 18.6 秒 |
最小が約 1.1 秒、最大が約 60.7 秒、中央値が約 36 秒という幅の広い分布です。これは、1 件の更新の遅延が「次の同期が来るまでの待ち時間(0〜60 秒)+ 同期 1 回ぶんの処理時間(約 1 秒)」で決まるためです。同期が走った直後に入った更新は次の同期まで約 60 秒待たされ、同期の直前に入った更新はすぐ取り込まれます。典型的なノコギリ波です。
ClickPipes 自身の Metrics でも、各同期バッチの「ソースから取得した行数(Rows pulled)」と「ClickHouse へ書いた行数(Rows pushed)」が一致し、直近バッチの所要時間が同期間隔とほぼ一致していることを確認できます。
5.2. interval を縮めると遅延は下がるが、マージが急増する
Sync interval を 60 / 30 / 10 / 1 秒と変えて、遅延と ClickHouse 側のパーツ / マージを採取しました。なお表の「アクティブ パーツ」は、いまテーブルを構成している生きたパーツの数です(マージで統合された古いパーツは含みません)。
| Sync interval | 遅延 最小 | 遅延 中央値 | 遅延 最大 | アクティブ パーツ | マージ(毎分) |
|---|---|---|---|---|---|
| 60 秒 | 1.09 秒 | 35.9 秒 | 60.7 秒 | 1 同期で +1 | 0 |
| 30 秒 | 1.43 秒 | 21.0 秒 | 30.6 秒 | +1(60 秒比 2 倍速で 3→5) | 0 |
| 10 秒 | 0.83 秒 | 6.9 秒 | 10.5 秒 | +1、5→1 でマージ発火 | 1(発火開始) |
| 1 秒 | 0.36 秒 | 0.99 秒 | 1.53 秒 | 約 5 で頭打ち | 4→50(マージ多発) |
読み取れることは次のとおりです。
- **遅延の最大値は、ほぼ Sync interval + 同期 1 回ぶんの処理時間(約 1 秒)**に一致します(60 秒→60.7 秒、30 秒→30.6 秒、10 秒→10.5 秒、1 秒→1.53 秒)。遅延は interval に支配されています
- 1 秒まで縮めれば、遅延は中央値約 1 秒・最小 0.36 秒まで下がります。サブ秒に届く更新も出てきます
- ただし、縮めるほど ClickHouse のマージが増えます。10 秒でマージが発火し始め、1 秒では毎分 50 件規模になりました。このときパーツ数は約 5 で横ばいでしたが、それはClickHouse が絶え間なくマージしてパーツ数を抑え込んでいるからで、裏で CPU / I/O を消費する「隠れた負荷」が発生しています
本検証では Sync interval だけを変え、Pull batch size など 2.1 の他の設定は既定のまま固定しています(interval 単体の効果を切り分けるため)。公式 Docs は、Sync interval と Pull batch size を併せて大きくすると一度に多くをまとめて取り込めてパーツ数を抑えられる、と述べており(sync_control)、実運用ではこれらを併せて調整することで、本章のトレードオフ(特にマージ負荷)を改善できる余地があります。
1 秒設定では、ClickPipes Metrics 上でも各同期が「20 行・所要 1.00 秒」で連続し、Pulling / Pushing が頻繁に切り替わる様子が見えます。なお、この「20 行」は本検証の書き込みレート(20 行/秒)× Sync interval(1 秒)で決まる値で、Pull Batch Size(100,000)の上限ではありません(1 インターバルに 10 万行を超える変更がたまって初めて上限が効きます)。
Sync interval は作成画面で 0 を入れると "Sync interval must be positive" で弾かれますが、1 は ClickPipes が受け付けます。推奨下限の 10 秒はあくまで推奨で、1 秒も設定できます。ただし上記のとおりマージ負荷が跳ね上がるため、自己責任での設定になります。
5.3. 遅延は 2 層ある(届くのは速いが dedup は遅い)
Sync interval = 1 秒で、既存行を UPDATE して新しい版を作り、FINAL を付けずに「旧版が消えて 1 行に確定するまで」を測りました。
- UPDATE がターゲットに届くまでの遅延は約 1.5 秒(1 秒間隔どおり速い)
- しかし dedup(重複排除)は 3 サンプルとも 180 秒以内に完了しませんでした。毎分 50 件のマージが走っている最中でも、その行の旧版と新版を含むパーツ同士が併合されず、
FINALなしのクエリでは重複が見え続けました
ReplacingMergeTree のマージは「いつ・どのパーツを併合するか」がスケジュール依存で、特定の行の dedup が短時間で終わる保証はありません。正しい最新行を読むには SELECT ... FINAL(または OPTIMIZE ... FINAL)が必須です。「届くのは速い(レプリケーション遅延)が、整合した最新状態(dedup 済み)はすぐには得られない」という 2 層構造が、実測でもはっきり出ました。
5.4. restart には強い(欠損ゼロ・重複ゼロ・自動再開)
Sync interval = 60 秒で、20 件 / 秒の連番(1,580 行)を流しながらコンソールから Restart を 1 回実行しました。
00:38:29 DOWN AdminShutdown: terminating connection due to administrator command
00:39:01 RECONNECTED after 31.909s
committed=1580 present_in_CH=1580 欠損=0 重複=0
FINAL での確定行数=1580
-
欠損ゼロ・重複ゼロで、コミットした 1,580 行すべてが ClickHouse に到達しました。
FINALでの確定行数も 1,580 で一致しています - レプリケーションスロットとパイプは手動介入なしで自動再開し、正しい位置(LSN)から無損失で再開しました。再起動は in-place(昇格なし)で、
AdminShutdownというエラーもこれを裏付けます - ただし、実ダウンタイムは約 32 秒でした
別途、ほぼ空のデータベース(2 vCPU)で restart を測ったときは、ダウンタイムは約 1 秒でした。今回は 2,000 万行のデータ+アクティブな CDC スロット+8 vCPU という条件で約 32 秒です。マネージドだからといって「再起動は一瞬」とは限らず、データ量やアクティブな論理レプリケーションスロットがあると数十秒落ちることがあります。
6. 考察
- 遅延は Sync interval に支配される。60 / 30 / 10 / 1 秒のいずれでも、遅延の最大値が「interval + 約 1 秒」に一致しました(5.2 章)。送出そのものは最小遅延(約 0.4〜1.4 秒)から見てサブ秒〜1 秒台で、遅延の大半は「次の同期を待つ時間」です
- 「サブ秒のレプリケーション遅延」はロードマップ=現時点では未提供と考えられます。公式ブログがこれを「今後」として挙げており(Blog、本記事執筆時点 2026-06)、実測でも既定 60 秒では中央値約 36 秒でした。1 秒に縮めれば約 1 秒には届きますが、次の論点とトレードオフです
- 速さの代償はマージ負荷。interval を縮めると毎分のマージ回数が跳ね上がり(60 秒で 0 → 1 秒で約 50)、パーツ数を抑えるための背景処理が増えます(5.2 章)。推奨下限が 10 秒に切られている理由は、このマージ増による ClickHouse 側の負荷にあると考えられます
-
dedup は別レイヤーの遅れで、FINAL が前提。届くのは速くても、ReplacingMergeTree の dedup は 180 秒たっても完了しないことがありました(5.3 章)。公式 FAQ の「dedup は非同期で一時的に重複が見える」と整合します。整合した最新状態を読むなら
FINALを使う設計が必須です - restart 耐性は高いが、ダウンタイムは条件依存。欠損ゼロ・重複ゼロで自動再開する一方(5.4 章)、ダウンタイムは空データベースの約 1 秒に対し、データ+スロットありで約 32 秒でした。運用設計では「無損失だが数十秒の瞬断はあり得る」と見ておくのが安全です
6.1. ClickPipes CDC と AWS Zero-ETL の仕組みの違い
「マネージドな CDC で列指向の分析基盤へ複製する」という発想は、AWS の Aurora PostgreSQL → Amazon Redshift の Zero-ETL と似ています。実際、変更を取り込む土台はどちらも Postgres の論理レプリケーションです(Aurora 側: AWS Blog / ClickPipes 側: Docs)。ただし、その先の作りはかなり違います。
| 観点 | AWS Zero-ETL(Aurora PG→Redshift) | ClickPipes CDC(Postgres→ClickHouse) |
|---|---|---|
| 変更取り込みの土台 | Postgres 論理レプリケーション | Postgres 論理レプリケーション(スロット) |
| WAL の保持・管理 | Aurora が専用ストレージ層に分離しエンジン側で隠蔽 | ソース上の標準スロット。停滞でソース側 WAL 逼迫が利用者に見える |
| 反映のタイミング | AWS が管理し連続的に反映(利用者は間隔を設定できない) | Sync interval ごとのバッチ(利用者が間隔を設定できる) |
| ターゲット側の見え方 | 統合で作られた読み取り専用テーブルに near real-time(数秒以内)で反映され、最新状態をそのまま読める(AWS Docs) | ReplacingMergeTree に版付きで積まれ、最新行の確定は後から(FINAL 前提) |
同じ「列指向ターゲットへの CDC」でも、トレードオフを AWS 側が隠すか、利用者に設定させるかという思想の違いがあります。本記事で実測できたのは ClickPipes 側だけで、Zero-ETL の内部の反映間隔などは非開示の部分が多く、公表値(数秒以内)に留まる点は補足しておきます。
7. まとめ
| ポイント | 実測 |
|---|---|
| 既定(60 秒)のレプリケーション遅延 | 中央値 約 36 秒 / 最大 約 60.7 秒(ノコギリ波) |
| 遅延の支配要因 | Sync interval。最大遅延 ≒ interval + 約 1 秒 |
| サブ秒は出るか | 1 秒に設定すれば中央値 約 1 秒。ただしマージが毎分 50 件規模に急増 |
| 速さの代償 | ClickHouse のマージ負荷(隠れた CPU / I/O 負荷) |
| dedup | 届くのは速いが dedup は 180 秒でも未完了。最新行は FINAL 必須 |
| restart 耐性 | 欠損ゼロ・重複ゼロで自動再開。ただしダウンタイムは約 32 秒(条件依存) |
ClickPipes の CDC は「設定した Sync interval ぶんだけ遅れて、まとめて届く」素直な作りでした。サブ秒を狙って interval を縮めることはできますが、その分だけ ClickHouse のマージが増えるトレードオフがあり、さらに「届くこと」と「重複が消えて整合すること」は別レイヤーです。一方で再起動への耐性は高く、無損失で自動再開しました。
8. 今後の課題
- 書き込み負荷下での遅延の伸び: 書き込みレートを上げると CDC が追いつかなくなる点があるはずですが、これはデータ量・テーブル構成・ターゲットのサイズに強く依存するため、どう測っても環境固有の参考値にしかなりません。条件を固定したうえでの「傾向」として別途測るかもしれません
- Sync interval 1 秒の長時間運用: マージが多発する状態を長時間続けたときに、パーツ数や "too many parts" がどう振る舞うか
- フェイルオーバーを跨いだ継続性: プライマリ障害でスタンバイが昇格する状況は、本記事執筆時点(2026-06・ベータ)では手動で起こす手段が確認できず、CDC が昇格を跨いで継続するかは未検証です(今回はスタンバイ 0 構成)
参考
- Postgres managed by ClickHouse | ClickHouse Blog
- Ingesting data from Postgres to ClickHouse (CDC) | ClickHouse Docs
- Controlling the syncing of a Postgres ClickPipe | ClickHouse Docs
- ClickPipes for PostgreSQL CDC(latency / replication lag の定義)| ClickHouse Docs
- ClickPipes for Postgres FAQ | ClickHouse Docs
- Amazon Aurora PostgreSQL zero-ETL integration with Amazon Redshift is generally available | AWS Blog
- Aurora zero-ETL integrations | AWS Docs

