Lakewright solutions / brief
Stream Postgres change data capture into Snowflake without Kafka or Snowpipe
Postgres inserts, updates and deletes land in an append-only Apache Iceberg changelog that Snowflake reads in place — no Kafka, no Debezium cluster, no Snowpipe, and no connector running inside your warehouse.
What Snowflake actually reads
Snowflake reads the landed table through an Iceberg external volume and a catalog integration — no copy, and no ingestion job on the Snowflake side. The credits you spend are the credits you spend querying, which is a different cost shape from a pipeline that pushes rows in continuously whether anyone reads them or not.
Each changelog row carries four control columns — _op (I/U/D/R), _position (the source LSN), the capture time, and the key — followed by the row image.
Current state without materialising anything
The cheapest correct answer is a view. Nothing is copied, nothing runs on a schedule, and it is never stale:
CREATE OR REPLACE VIEW orders AS SELECT * EXCLUDE (_op, _position, _captured_at_ms, _key) FROM orders_changes QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY _position DESC) = 1 AND _op <> 'D';
Latest row per key, deletes dropped. For dimension-sized tables and dashboards that run a few hundred times a day this is usually where the argument ends — you have current state and you paid nothing to keep it.
Current state as a real table
When the changelog is large enough that re-deriving state on every query stops being free, materialise it. A Dynamic Table keeps the same SQL and hands Snowflake the refresh problem:
CREATE OR REPLACE DYNAMIC TABLE orders TARGET_LAG = '5 minutes' WAREHOUSE = loading AS SELECT * EXCLUDE (_op, _position, _captured_at_ms, _key) FROM orders_changes QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY _position DESC) = 1 AND _op <> 'D';
A stream plus a task running MERGE is the classic equivalent and behaves identically. Which you choose is a Snowflake question, not a Lakewright one: the changelog is the same table underneath, and you can change your mind later without re-ingesting anything.
Why the changelog is the thing we ship
A mirrored table overwrites history by definition. The changelog keeps it, so the current-state view is a derivation you can always rebuild — and the intermediate states, which is where most of the interesting questions live, are still there. Deletes are appended _op=D rows rather than destructive deletes, which is the form auditors ask for.
FAQ
The question that matters
"How long does an order actually sit in 'pending' before it ships, and is that getting worse?"
The current-state table cannot answer this, and no amount of warehouse compute will change that: it knows the order is shipped now, and has forgotten every moment before that. The duration only exists in the transitions.
Against the changelog it is a gap between two events per key, and in the sample data the median is unremarkable while the mean is roughly twice it — the familiar signature of a small tail dragging an average around. The tail is the finding. It concentrates in a single fulfilment path rather than spreading evenly, which turns "our shipping is slow" into a specific question for a specific owner, and the trend line says whether last quarter's fix actually held.
Current-state tables tell you what is true now. The changelog tells you what happened, and how long it took, which is usually where the money is.
Want this run against your data?
We start with a read-only scan and an inventory report — no installation on your systems, nothing leaves your environment. Most engagements produce findings the team didn't know about in the first afternoon.