Materialized Views
Push-based pre-aggregation
In Postgres, a materialized view is a query result you periodically
REFRESH — it goes stale between refreshes. A ClickHouse
materialized view is a completely different mechanism: it's an
insert trigger that runs a query against every newly inserted
block of rows and pushes the output into a separate target table,
continuously, forever.
The mental model: push, not pull
Crucially, the view's SELECT never runs against the whole source
table — only against the rows in the block that was just inserted.
The cost of maintaining it is proportional to insert volume, not to
the size of the source table.
The classic pattern: pre-aggregation
CREATE TABLE events
(
event_time DateTime,
event_type LowCardinality(String)
)
ENGINE = MergeTree ORDER BY event_time;
CREATE TABLE events_per_minute
(
minute DateTime,
event_type LowCardinality(String),
cnt AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
ORDER BY (minute, event_type);
CREATE MATERIALIZED VIEW events_per_minute_mv
TO events_per_minute
AS
SELECT
toStartOfMinute(event_time) AS minute,
event_type,
countState() AS cnt
FROM events
GROUP BY minute, event_type;Reads against events_per_minute stay fast forever, because they
scan pre-aggregated per-minute rows instead of raw events — even
after the source table grows into the billions.
A materialized view only sees rows inserted directly into its source table. If another materialized view (or a distributed insert path) writes into that table indirectly, or if you insert straight into the target table, the trigger may not fire the way you expect. Always trace where writes actually land.
The target table uses AggregateFunction(count) and
AggregatingMergeTree, not a plain integer with
SummingMergeTree — this lets partial aggregate states from
different inserts merge correctly later. Reading it back requires
countMerge(), not a plain SUM.
Materialized view vs. projection
Use a materialized view when you want a differently shaped result — aggregated, filtered, joined — living in its own named table you query explicitly. Use a projection when you want the same rows just sorted differently, chosen automatically under the same table name.