Page under construction
Real-time analytics, end to end.
Explore a ClickHouse analytics pipeline, one stage at a time, from sources to dashboards.
Example 1 Β· Multi-tenant SaaS
Live usage dashboards for every customer #
Each customer sees its own live usage. Five stages take an event from its source to a customer's chart; pick one to see how.
Select a stage to explore
ClickHouse stages 2β4
01 Ingest Data comes in
#ingestThe batch lands as one new part.
Docs βA stream buffers events and can replay them.
Databases, and the tools that copy each change (CDC).
Files in object storage, or lake tables read in place.
SQL & details
One minute of this pipeline: every source arrives in batches, and each batch becomes a new part.
- Live events (Kafka)
- A batch every 7.5 s by default (
stream_flush_interval_ms); on ClickHouse Cloud, ClickPipes batches for you. - Database changes (PostgreSQL)
- ClickPipes (PeerDB) copies the table once, then adds each change as a new row, every 60 s by default.
- History files (S3)
- Loaded once with
INSERT INTO events SELECT β¦ FROM s3(β¦), a few days per insert: one insert may touch at most 100 partitions. - Small app writes
- Asynchronous inserts batch them on the server, on by default since 26.2 (2026-02) (Guide 03).
02 Raw tables Kept as it arrives
#rawMergeTree keeps every event.
Docs βKeep raw events long enough to rebuild the totals, then expire them with TTL.
A retried insert must not count twice in the totals.
SQL & details
Each table engine decides what a background merge does with rows that share a sorting key. Until merges run, read with FINAL or aggregate (Guide 05).
- What counts as a duplicate
- Rows combine only when their whole
ORDER BYmatches, sotenantsis sorted by its key alone. - Other engines
AggregatingMergeTreefor distinct counts and percentiles,CollapsingMergeTree(both in Guide 02);Nullstores nothing but still feeds views.- Fixing a few rows
UPDATEwrites a small patch part when the table hasenable_block_number_columnandenable_block_offset_column.
03 Transform Summed ahead of time
#transformTotals add up as they land: $40 + $30 = $70.
Docs βScheduled SQL jobs, or stream processing before ClickHouse.
SQL & details
A materialized view runs on every insert, so the hourly totals are ready as soon as the data lands. The figure sets this against a join refreshed every hour.
- The view behind the hourly totals
CREATE TABLE usage_hourly (tenant_id UInt32, hour DateTime, β¦)ENGINE = SummingMergeTreePARTITION BY toYYYYMM(hour)ORDER BY (tenant_id, hour, device_type) CREATE MATERIALIZED VIEW usage_mv TO usage_hourly ASSELECT tenant_id, toStartOfHour(timestamp) AS hour, device_type, count() AS events, sum(revenue) AS revenueFROM events GROUP BY tenant_id, hour, device_type- Partial totals add up
SummingMergeTreeadds up the partial rows when parts merge, and queries stillsum(). For distinct counts, useAggregatingMergeTree.- Joins inside a view
- They use the other table as it was at insert time. For the current plan, join when a chart asks, in the metrics layer.
- One row per tenant
- The figure's second way: two views fill one
CoalescingMergeTreerow, which keeps each column's latest value. - When a schedule still fits
- Work that needs all the data at once, such as rankings: a refreshable view,
REFRESH EVERY 1 HOUR. - Backfill
- A new view sees only new inserts: fill the past with one
INSERT β¦ SELECTper tenant (Guide 06).
04 Serving table What the charts read
#servingOnly a few granules, and only the needed columns, are read.
Docs βPut the filters every chart uses first: customer, then time.
Keep heavy internal queries away from customer charts.
SQL & details
Today's chart for tenant 42 needs at most 120 totals: 24 hours Γ 5 devices. Each cut in the figure leaves less to read.
- Check the cuts
EXPLAIN indexes = 1prints them: 4 of 7 parts, then 4 of 1,280 granules (Overview).- Pre-summed
- Once merged,
usage_hourlyhas at most 720 k rows a day, against 151 M events. - All tenants at once
- The figure's projection,
by_hour, keeps that rollup inside the table; onSummingMergeTreeit needsdeduplicate_merge_projection_mode = 'rebuild'. - Repeated charts
- The query condition cache is on by default; the query cache is opt-in and keeps results for 60 s (Guide 07).
05 Semantic Β· BI What people see
#biEach customer sees only its own rows.
Docs βDefine each metric once, and which rows each customer may see.
Embed a dashboard tool, or build your own UI on the metrics layer.
Connect directly; choose live queries or imported data.
SQL & details
The metrics layer turns a chart into SQL, adds the customer's filter and caches answers. system.query_log records how long ClickHouse took.
- Charts for customers
- Take the tenant from the signed-in user, on the server; never trust a filter sent by the browser.
- Dashboards for your team
- Give the tool its own database user, with limits. Power BI stays live in DirectQuery mode.
- Speed: the slowest charts
SELECT normalized_query_hash, any(query) AS chart, quantile(0.95)(query_duration_ms) AS p95_ms, max(read_rows) AS max_rows, count() AS runsFROM system.query_logWHERE type = 'QueryFinish' AND user = 'cube' AND event_time > now() - INTERVAL 1 HOURGROUP BY normalized_query_hashORDER BY p95_ms DESC LIMIT 10p95 shows the slow runs an average hides. Cache hits never reach ClickHouse, so also time the page in the browser.
- Freshness
- A new event reaches a chart in about 18 s (7.5 s batch, then up to 10 s in the cache), a plan change in about 70 s. Alert when
now() - max(timestamp)grows past that.