Skip to main content
Filament copies PostgreSQL tables into typed ClickHouse tables in two ways. A full replace snapshot builds each table in a staging table and swaps it in atomically. An incremental upsert reads only rows whose updated_at moved and writes them into a ReplacingMergeTree table keyed on the primary key. In the run below, Filament loaded 1,105,000 rows from PostgreSQL 16 into ClickHouse 25.8 in about 5 seconds, then picked up 1,500 changed rows on the next run.
The ClickHouse sink is alpha and the PostgreSQL source is beta. The ClickHouse sink does not support CDC, so deletes in PostgreSQL are not removed from ClickHouse. Use a periodic full replace when deletes matter.

What you’ll build

An e-commerce database with customers (100,000 rows), products (5,000), and orders (1,000,000) replicated into ClickHouse:

Prerequisites

  • The filament CLI. See Installation.
  • PostgreSQL, reachable from where Filament runs
  • ClickHouse, self-hosted or ClickHouse Cloud
  • For incremental tables, a primary key and a NOT NULL timestamp column that your application updates on every insert and update, such as updated_at
Full and incremental pipelines run on the local CLI context, so you can follow this guide on a laptop.

1. Create a read-only PostgreSQL user

2. Create the source and sink

The sink defaults to the secure native port 9440 with TLS, which is what ClickHouse Cloud expects. For a local self-hosted server without TLS, add --sink-port 9000 --sink-secure=false. The ClickHouse user needs access to the default database, because Filament runs CREATE DATABASE IF NOT EXISTS from there. Check what Filament can read:

3. Snapshot the tables

filament run ls pg-to-ch-snapshot showed a duration of 5 seconds for the 1.1M rows. Each table is written to a staging table and swapped in with EXCHANGE TABLES at commit, so queries never see a half-loaded table. Filament maps PostgreSQL types to native ClickHouse types and orders the table by the primary key:

4. Keep large tables fresh with incremental upserts

The first run backfills the whole table (1,000,000 rows in under 6 seconds in this test) and records a high watermark on (updated_at, id). Filament picks updated_at automatically because it is a conventional cursor name. Now change data in PostgreSQL:
Run the pipeline again:
The run read the 1,500 changed rows plus rows inside the default 300-second lookback window, which re-reads recent rows to catch late commits. Upserts make the overlap harmless.

5. Verify

ReplacingMergeTree removes older versions of a row during background merges, so a plain count() can briefly include duplicates. Query with FINAL for exact results:
Compare totals with PostgreSQL:
In this test both sides returned identical counts and sums for every status.

6. Schedule it

Add a cron schedule to each pipeline in the web app (filament up on a laptop, or your deployed server), or attach one through the Pipelines API. A common setup is the incremental pipeline every 5 to 15 minutes and the snapshot pipeline nightly.

Going to production

  • Deletes: incremental reads cannot see hard deletes, and the ClickHouse sink has no CDC. Schedule a full replace of the table, or soft-delete in PostgreSQL with a deleted_at column that the incremental read picks up.
  • Cursor index: for large tables, an index on (updated_at, id) keeps incremental reads fast.
  • Visibility: replace loads become visible at commit. Append and upsert loads write to the live table, so rows appear while the run is in progress.
  • Schema changes: new PostgreSQL columns are added to ClickHouse tables. Type changes and dropped columns are not applied.

Troubleshooting

A key has more than one row in ClickHouse

Older versions are removed when parts merge. Query with FINAL, or run OPTIMIZE TABLE shop_live.orders FINAL to force a merge.

Incremental mode rejects a table

The table needs a primary key and a NOT NULL timestamp cursor that advances on every write. Static IDs and random UUIDs are not valid cursors.

Rows deleted in PostgreSQL are still in ClickHouse

Expected with incremental upserts. See Deletes above. More errors are in Troubleshooting.

FAQ

Can Filament stream PostgreSQL changes to ClickHouse with CDC?

Not yet. The ClickHouse sink supports full replace, full append, full upsert, and incremental upsert. For change data capture with deletes, replicate to a sink that supports cdc_merge, such as PostgreSQL, Snowflake, or BigQuery.

How fast is it?

In this guide’s test, 1.1M rows from PostgreSQL 16 loaded into ClickHouse 25.8 in about 5 seconds on one machine. For a controlled comparison with other tools, see Benchmarking.

Does it work with ClickHouse Cloud?

Yes. ClickHouse Cloud uses the secure native port 9440 with TLS, which are the sink’s defaults.