
What is CDC in data engineering? Change data capture explained
What is CDC in data engineering? Learn how change data capture records database changes, differs from ETL and incremental loading, and when to use it.
Learn how to load Shopify data into ClickHouse, choose an ingestion method, model a bronze-silver-gold ecommerce warehouse, and use Bruin to spend less time maintaining data tooling.

Arsalan Noorafkan
Developer Advocate

Quick answer: ClickHouse is a strong home for a Shopify analytics warehouse when you need fast, repeatable answers over orders, line items, customers, products, inventory, and events. Load the Shopify data with the ingestion method that fits your team, keep a source-shaped raw layer, then publish cleaned models and reporting tables. ingestr is a simple, fast CLI option for the first step. To make this concrete, we will use Bruin's out-of-the-box ecommerce template as a running example to explain how to ingest, transform, and model Shopify data in ClickHouse.
The first Shopify sync is rarely the hard part. The hard part is what happens afterwards.
An order is edited. A partial refund lands three weeks later. A product is archived. Someone asks why the revenue chart no longer matches Shopify. The answer is often trapped in a connector setting, a dashboard calculation, or a very brave CASE WHEN that nobody wants to touch.
This is why a Shopify to ClickHouse setup needs more than a connector. It needs a warehouse shape, explicit business rules, and a way to change both without turning every new metric into a mini migration project.
Shopify has a familiar ecommerce data shape: lots of detailed records, a few very common reporting paths, and regular changes to existing records. People ask for revenue by day, product performance, returning-customer rate, discount usage, refunds, inventory risk, and attribution. Then they ask for the same answers by country, channel, currency, collection, customer cohort, or marketing campaign.
ClickHouse is built for this kind of analytical workload. Its MergeTree table family stores data in sorted parts, supports high ingest rates, and uses a sparse primary index to skip data during reads. The physical order of a table matters, which is useful when the queries are predictable enough to model around them.
For a Shopify warehouse, that usually means:
Decimal and timestamps as UTC DateTime64There is one ClickHouse detail worth getting right early: ORDER BY is not a uniqueness constraint. It defines the sorting key and, in a MergeTree table, the sparse index. Choose it for the way people read the data, not because it looks like a familiar OLTP primary key. ClickHouse's own guidance also cautions against overly granular partitions. A monthly partition is often enough; partitioning by store or customer ID is usually a bad trade. Read the MergeTree partitioning guidance.
You do not need every Shopify object on day one. Start with the data that supports the decisions the business already makes.
| Shopify resource | What it unlocks | Common grain |
|---|---|---|
| Orders | Revenue, order status, discounts, taxes, fulfilment | One row per order |
| Order line items | Product demand, units sold, discount allocation | One row per order item |
| Customers | New versus returning buyers, cohorts, lifetime value | One row per customer |
| Products and variants | Product catalogue, vendor, collection, option analysis | One row per product or variant |
| Inventory items | Stock, sellable inventory, unit cost when available | One row per inventory item or snapshot |
| Transactions and refunds | Payment status, tender reconciliation, refund analysis | One row per transaction or refund |
| Discounts and events | Promotion definitions and source audit history | One row per source record |
Shopify's source reference lists orders, customers, products, inventory_items, discounts, transactions, and events as available data sets. That is plenty for a proper first version.
The useful question is not "what can the API give us?" It is "what has to be true for this report to be trusted?" If a finance report needs payment status, get transactions. If the merchandising team needs stockout analysis, take an inventory snapshot. If neither question is in scope, do not build it just because the endpoint exists.
There is no single best way to ingest Shopify data into ClickHouse. The right choice depends on how much connector maintenance you want to own, how often data needs to refresh, how much control you need over the process, and whether ingestion is one isolated job or part of a broader pipeline.
| Method | Good fit | Trade-off |
|---|---|---|
| Managed ELT connector | You want someone else to operate the Shopify API sync | Less control and recurring platform cost |
| Open-source connector platform | You need a wide connector catalogue and can run infrastructure | Connector quality and maintenance vary |
| Custom Shopify API integration | You have unusual source logic or a narrow, stable use case | You own rate limits, retries, schema changes, and backfills |
ingestr CLI | You want a fast, scriptable ingestion path without writing connector code | It handles ingestion, not your full transformation and governance workflow |
| A pipeline framework such as Bruin | You want ingestion, SQL/Python models, checks, and dependency management together | You still need to design the warehouse and business definitions |
ingestr is a good option when the goal is simple: move data reliably, run it from a terminal or CI, and keep the configuration close to the project. Its interface is deliberately small: provide a source and destination, choose an incremental strategy when the source supports one, then let the tool move the records. It can also run inside a Bruin project as an ingestr asset.
For Shopify, incremental loading matters more than a clever first backfill. Orders, customers, products, and discounts all change after their first appearance. Use the source updated_at field and a merge-style write where it is supported. Shopify's source documentation identifies updated_at and merge as the natural incremental pattern for the main commerce resources. See the Shopify ingestion reference.
Medallion architecture is just a practical way to stop source data, cleaning logic, and business metrics from becoming one big table. The labels vary. You might call the layers bronze, silver, and gold, or raw, staging, and marts. The separation is what matters.
Shopify Admin API
|
v
bronze: source-shaped records and ingestion metadata
|
v
silver: clean orders, line items, customers, products, inventory
|
v
gold: daily revenue, cohorts, product performance, reconciliation
| Layer | What belongs there | What should not happen there |
|---|---|---|
| Bronze | Shopify records, source IDs, update timestamps, ingestion timestamps, raw nested payloads when needed | Revenue definitions hidden inside an ingestion job |
| Silver | Typed columns, flattened line items, standardized statuses, reusable order and customer logic | Dashboard-specific totals and ad-hoc exclusions |
| Gold | Small, business-facing tables with documented metric definitions | Raw source parsing or personal data that the report does not need |
The bronze layer is a recovery point. It should keep Shopify close enough to the source that you can inspect an odd order, replay a downstream model, or adapt to a source field that appears later.
Each raw table should have the source identifier, the Shopify update timestamp, the ingestion timestamp, and the source fields or raw payload that downstream models need. For example, bronze.shopify_orders may keep the order header and nested line-item structure, while bronze.shopify_transactions keeps payment and refund events separately.
ReplacingMergeTree can be useful when an ingestion process writes a newer version of the same Shopify record. But it is not a magic upsert guarantee. De-duplication happens during background merges, so duplicate versions may remain visible for a while. ClickHouse documents this as eventual correctness and recommends query-time de-duplication where an exact result requires it. Read the ReplacingMergeTree behaviour.
That means a revenue dashboard should not depend on background de-duplication happening at the right moment. Treat the bronze table as source history or the latest available source state, then make the silver and gold refresh rules explicit.
Raw Shopify orders are not analytics tables. They contain nested line items, shipping addresses, fulfilments, discounts, refunds, and several status fields that look similar until they do not. The silver layer gives those records stable names and a single grain.
A practical Shopify silver layer usually includes:
silver.orders with one row per Shopify ordersilver.order_line_items with one row per order line itemsilver.customers with customer lifecycle fields and only the personal data you genuinely needsilver.products and silver.product_variants with catalogue contextsilver.inventory_snapshots when inventory history matterssilver.transactions and silver.refunds when finance needs reconciliationThis is where you flatten line items, cast timestamps, normalize status values, and state the business rules. Keep gross_sales, discount_amount, refund_amount, and net_revenue separate. Preserve the source currency. Exclude test or cancelled orders through named flags instead of copying the same conditions into every downstream query.
One boring but important rule: never sum different currencies in the same metric. If leadership needs a single reporting currency, add an explicit FX model with a documented rate source and date policy. A chart that silently adds USD, EUR, and CAD is worse than no chart at all.
Gold tables should answer a business question directly. They are not a place to dump every cleaned field in the warehouse.
| Gold table | Useful dimensions and measures |
|---|---|
gold.daily_revenue | date, currency, sales channel, gross sales, discounts, refunds, net revenue, completed orders, AOV |
gold.customer_cohorts | first-order month, activity month, active customers, repeat orders, retained revenue |
gold.product_performance | date, product, variant, units, gross sales, refund-adjusted revenue, inventory state |
gold.payment_reconciliation | financial status, transaction amount, order amount, refund amount, reconciliation difference |
gold.discount_performance | discount code, campaign, orders, discount cost, net revenue |
Here is a deliberately small daily_revenue shape:
CREATE TABLE gold.daily_revenue
(
metric_date Date,
currency LowCardinality(String),
completed_orders UInt64,
gross_sales Decimal(18, 2),
discounts Decimal(18, 2),
refunds Decimal(18, 2),
net_revenue Decimal(18, 2)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(metric_date)
ORDER BY (currency, metric_date);
The table is compact because the complicated work already happened in silver. When an order changes, recompute only the affected date range or partition. That is generally safer than trying to patch every aggregate in place.
ClickHouse gives you useful choices, but each one has a cost. A Shopify analytics warehouse does not need every optimisation on the first day.
| ClickHouse feature | How it helps an ecommerce warehouse | Practical advice |
|---|---|---|
ORDER BY sorting key | Lets the sparse index skip data for common filter patterns | Put frequently filtered, selective dimensions first. Design from real queries, not table diagrams. |
PARTITION BY | Makes date-bound maintenance and pruning easier | Partition time-series facts by month in most cases. Avoid tenant- or customer-level partitions. |
ReplacingMergeTree | Holds newer versions of mutable Shopify records | Use it with an explicit version and know that background merges are eventual. |
LowCardinality(String) | Reduces the cost of repeated low-distinct-value strings such as currency, country, channel, or status | Use it for real enums and recurring labels, not every string column. |
Decimal and DateTime64 | Prevents floating-point money surprises and keeps event ordering precise | Store timestamps in UTC and convert for display at the edge. |
| Materialized views | Can precompute a simple insert-driven aggregate | Use with care for mutable order data. Scheduled or bounded refreshes are usually easier to reason about for refunds and edits. |
| TTL | Can remove or move older raw data automatically | Apply it only after agreeing on audit, finance, and privacy retention needs. |
The right model is tied to the query pattern. A product-performance query filtering by date and product should not use the same sort order as a customer-cohort table filtering by first-order month. ClickHouse's query optimisation guide is a useful reference once you have production query logs rather than guesses.
The dangerous warehouse bugs are often reasonable-looking numbers. A pipeline needs checks close to the SQL that produces the metric.
For a Shopify to ClickHouse pipeline, start with rules like these:
These rules make the warehouse easier for people to maintain. They also make it much easier for an AI analyst or data agent to answer a question without guessing what revenue means. The definition, dependencies, and validation rule all live next to the implementation.
You do not need Bruin to use ClickHouse, and you do not need to replace a working ingestion tool just to adopt a medallion architecture. Bruin is useful when the tooling around the warehouse starts to sprawl: one job for extraction, another repository for SQL, a third place for checks, and no clear way to see what an edited order affects.
Bruin is an open-source CLI that runs SQL and Python assets, supports data-quality checks, and includes ingestr as a native ingestion asset. A small Shopify orders asset can look like this:
name: bronze.shopify_orders
type: ingestr
connection: clickhouse-default
materialization:
type: table
strategy: merge
incremental_key: updated_at
parameters:
source_connection: shopify-default
source_table: orders
destination: clickhouse
The asset describes the source, destination, incremental behaviour, and ownership in one reviewable file. From there, SQL assets can build the silver and gold layers, while checks make the metric contracts executable. Bruin can also show the dependencies between those assets, so a change to the Shopify order model has an obvious downstream blast radius.
If you are starting fresh, the Bruin ecommerce template scaffolds Shopify ingestion, cleaned models, and reporting tables for ClickHouse, BigQuery, or Snowflake. The workflow is deliberately straightforward:
bruin init ecommerce
bruin validate ./ecommerce
bruin run ./ecommerce
The template is not the architecture. It is a fast, readable example of one. Keep the pieces that match the questions your business needs to answer, delete the ones that do not, then make the data rules more specific as the team learns what it trusts.
ClickHouse gives Shopify analytics a fast analytical engine. A medallion structure keeps that engine understandable. And a tool such as Bruin can keep the ingestion, SQL, checks, and lineage together so the team spends less time nursing data tooling and more time deciding what the data should change.

What is CDC in data engineering? Learn how change data capture records database changes, differs from ETL and incremental loading, and when to use it.

What is CDC streaming? Learn how it reads database logs, where Kafka fits, how it differs from batch CDC, and when its operational cost is worth it.

A practical comparison of dlt alternatives for data ingestion: Bruin CLI, ingestr, Airbyte, Sling, Meltano, and Fivetran, including a MongoDB to Postgres benchmark.
Practical updates on open-source data pipelines, AI analysts, governance, and what we are shipping at Bruin.