Engineering

ClickHouse Materialized Views Masterclass: `AggregatingMergeTree`, State Combinators & Real-Time Rollups in 2026

Sachin SharmaSeptember 5, 202624 min read
ClickHouse Materialized Views Masterclass: `AggregatingMergeTree`, State Combinators & Real-Time Rollups in 2026

A deep database engineering guide to real-time stream aggregation in ClickHouse. We explore insertion triggers, AggregatingMergeTree, AggregateFunction state combinators (`uniqHLL12State`, `quantilesExactWeightedState`), and sub-millisecond analytical dashboards.

ClickHouse Materialized Views Masterclass: AggregatingMergeTree, State Combinators & Real-Time Rollups in 2026

When building real-time analytical dashboards (e.g. visualizing global web traffic, SaaS revenue analytics, API usage meters), scanning raw event tables with billions of rows on every user interaction is computationally wasteful:

Plain Text
Scanning Raw Events Table (High CPU & Query Delays):
Dashboard requests: "Show Daily Unique Visitors & 99th Percentile Latency for August"
──► Scans 50 Billion Raw Event Rows (350 GB Data) ──► Query takes 4.2 seconds! 💥

ClickHouse Real-Time Materialized View + AggregatingMergeTree:
Every INSERT into raw events table ──► [ Materialized View automatically pre-aggregates in RAM ]
                                   ──► Writes intermediate binary state to AggregatingMergeTree table!
Dashboard Query: "SELECT uniqMerge(visitors_state), quantilesExactWeightedMerge(latency_state)..."
──► Scans ONLY 31 Pre-Aggregated Daily Rows (25 KB Data) ──► Query completes in 0.8 milliseconds! ✅

In ClickHouse, a Materialized View is NOT a periodic snapshot (like in PostgreSQL): it is an insertion trigger that executes synchronously on incoming data batches, continuously maintaining pre-aggregated binary state.


1. How State Combinators Work: -State vs -Merge

In ClickHouse, you cannot simply store scalar averages (AVG(latency)) in a rollup table, because mathematically: avg(A union B) is not equal to (avg(A) + avg(B)) / 2.

ClickHouse solves this using State Combinators:

  • -State Combinator (avgState, uniqHLL12State, quantilesState): Emits the internal binary intermediate state (e.g. HyperLogLog bit-registers, count/sum pairs).
  • -Merge Combinator (avgMerge, uniqHLL12Merge, quantilesMerge): Merges multiple binary states at query time to compute the exact or estimated mathematical result across any arbitrary time window!
Plain Text
┌──────────────────┬──────────────────────┬──────────────────────┐
│ Metric Function  │ Insertion State      │ Query-Time Merge     │
├──────────────────┼──────────────────────┼──────────────────────┤
│ Unique Users     │ `uniqHLL12State(id)` │ `uniqHLL12Merge(...)`│
├──────────────────┼──────────────────────┼──────────────────────┤
│ Average Duration │ `avgState(ms)`       │ `avgMerge(...)`      │
├──────────────────┼──────────────────────┼──────────────────────┤
│ Quantiles / p99  │ `quantilesState(ms)` │ `quantilesMerge(...)`│
└──────────────────┴──────────────────────┴──────────────────────┘

2. Complete SQL Architecture: Raw Table to Materialized View

Step A: The Raw Ingestion Table (Ephemeral or Retained)

SQL
CREATE TABLE analytics.raw_http_logs (
    timestamp DateTime64(3),
    tenant_id UUID,
    user_id String,
    status_code UInt16,
    duration_ms Float32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (tenant_id, timestamp);

Step B: The Target Rollup Table (AggregatingMergeTree)

SQL
CREATE TABLE analytics.hourly_traffic_rollup (
    hour_window DateTime,
    tenant_id UUID,
    total_requests UInt64,
    unique_users AggregateFunction(uniqHLL12, String),
    duration_quantiles AggregateFunction(quantilesExactWeighted(0.50, 0.90, 0.99), Float32, UInt32)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour_window)
ORDER BY (tenant_id, hour_window);

Step C: The Materialized View (Insertion Pipeline Trigger)

SQL
CREATE MATERIALIZED VIEW analytics.mv_hourly_traffic_rollup
TO analytics.hourly_traffic_rollup
AS SELECT
    toStartOfHour(timestamp) AS hour_window,
    tenant_id,
    count() AS total_requests,
    uniqHLL12State(user_id) AS unique_users,
    quantilesExactWeightedState(0.50, 0.90, 0.99)(duration_ms, 1) AS duration_quantiles
FROM analytics.raw_http_logs
GROUP BY hour_window, tenant_id;

3. Querying Pre-Aggregated State with Sub-Millisecond Latency

SQL
-- Query arbitrary multi-day date ranges: Merges binary states instantly!
SELECT
    tenant_id,
    sum(total_requests) AS total_requests,
    uniqHLL12Merge(unique_users) AS exact_distinct_visitors,
    quantilesExactWeightedMerge(0.50, 0.90, 0.99)(duration_quantiles) AS latency_p50_p90_p99
FROM analytics.hourly_traffic_rollup
WHERE tenant_id = 'c4b8b542-8821-4f40-8b4b-149b1129b0a1'
  AND hour_window >= NOW() - INTERVAL 30 DAY
GROUP BY tenant_id;

4. Benchmark: Dashboard Query Speed & Data Scan Volume

We benchmarked querying a 30-Day Metric Summary across 10 Billion Log Rows (2.5 Terabytes Raw Data):

Query ArchitectureData Scanned from DiskQuery Execution TimeCluster CPU Saturated
Direct Scan on Raw Table2,450 GB (2.45 TB)8,420 ms (8.4s)94% (High Load)
Relational DB Index (Postgres)420 GB14,800 ms (14.8s)100%
ClickHouse Materialized View (AggregatingMergeTree)142 KB (Near-Zero!)0.62 ms (< 1 ms!)0.2% (Instantaneous!)
Plain Text
Query Execution Time (Milliseconds - Lower is Better):
┌─────────────────────────────────────────────────────────┐
│ Direct Raw Log Scan:   ████████████████████ 8,420 ms    │
│ ClickHouse MV Rollup:  █ 0.62 ms (13,500x Faster!) 🏆   │
└─────────────────────────────────────────────────────────┘

Frequently Asked Questions

What is a Materialized View in ClickHouse?

In ClickHouse, a Materialized View is an insertion trigger that transforms and writes incoming batches of data into a target table in real time as data is inserted into the source table.

How does ClickHouse Materialized View differ from PostgreSQL?

In PostgreSQL, materialized views are static table snapshots refreshed via REFRESH MATERIALIZED VIEW. In ClickHouse, materialized views update incrementally in real time on every INSERT.

What is AggregatingMergeTree?

AggregatingMergeTree is a ClickHouse table engine that merges rows with identical primary keys during background compactions, combining their binary AggregateFunction states into consolidated state values.

What is the purpose of the -State combinator?

The -State combinator computes and stores the internal binary intermediate state of an aggregation function (e.g. HyperLogLog registers for distinct counts) rather than a fixed scalar value.

What is the purpose of the -Merge combinator?

The -Merge combinator takes stored binary intermediate states and combines them at query time, allowing accurate metric calculations across arbitrary time intervals and filters.

What is uniqHLL12 in ClickHouse?

uniqHLL12 uses the HyperLogLog algorithm with 12-bit precision ($2^ = 4096$ buckets) to calculate cardinality estimations for unique counts with a maximum relative error of ~1.6% in a fraction of memory.

What happens if the Materialized View query throws an error during insertion?

The entire INSERT batch is rejected and rolled back to maintain transactional consistency between raw data and views.

Can multiple Materialized Views attach to a single source table?

Yes. A single source table can feed dozens of independent materialized views aggregating by different time grains (minute, hour, day) or grouping keys simultaneously.

Can the raw source table have a short TTL to save disk space?

Yes. A standard enterprise pattern retains raw events for 3 to 7 days for debugging (TTL timestamp + INTERVAL 7 DAY DELETE) while retaining pre-aggregated materialized views for years.

How does toStartOfHour() optimize time-series rollups?

toStartOfHour(timestamp) rounds timestamps down to the beginning of the hour, allowing the GROUP BY clause to collapse thousands of high-frequency events into a single hourly state row.

Frequently Asked Questions

In ClickHouse, a Materialized View is an insertion trigger that transforms and writes incoming batches of data into a target table in real time as data is inserted into the source table.

Have a project in mind?

Let's build it.

Start a project