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 withcustomers (100,000 rows), products (5,000), and
orders (1,000,000) replicated into ClickHouse:
Prerequisites
- The
filamentCLI. See Installation. - PostgreSQL, reachable from where Filament runs
- ClickHouse, self-hosted or ClickHouse Cloud
- For incremental tables, a primary key and a
NOT NULLtimestamp column that your application updates on every insert and update, such asupdated_at
1. Create a read-only PostgreSQL user
2. Create the source and sink
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
(updated_at, id). Filament picks
updated_at automatically because it is a conventional cursor name.
Now change data in PostgreSQL:
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:
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_atcolumn 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 withFINAL, or run
OPTIMIZE TABLE shop_live.orders FINAL to force a merge.
Incremental mode rejects a table
The table needs a primary key and aNOT 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 supportscdc_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 port9440 with TLS, which are the
sink’s defaults.