Module 43 / 46 · Phase H — Streams & Data Pipelines · finale · 60 min

Real-time
analytics.

The last hop. Streams built by M.39-M.42 need to become queryable — user-facing dashboards, live analytics, sub-second aggregations. Two families answer this: streaming SQL (Materialize, RisingWave) maintains a query result up-to-date incrementally; real-time OLAP (Pinot, Druid, ClickHouse) does columnar storage with heavy pre-aggregation over streaming ingestion. Both are wrong choices for the workload the other one is designed for. Which one you need depends on the query shape — and the answer isn't always which one is faster.

// What you'll know by the end

  • Streaming SQL · real-time OLAP · two families
  • Incremental View Maintenance mechanism
  • Materialize · RisingWave · Pinot · Druid tradeoffs
  • End-to-end: CDC → Kafka → Flink → Analytics → dashboards
§ 01 — The last hop

Streams must
become queryable.

Every module in Phase H has been about processing streams correctly — batching them (M.39), windowing them (M.40), making them stateful (M.41), sourcing them from operational databases (M.42). What we haven't discussed until now is what streams are ultimately for. In a modern data platform, the answer is user-facing: dashboards that update in real-time as events flow through, analytics APIs that return sub-second query responses over live data, materialized views that stay fresh without periodic rebuilds. The streaming pipeline exists to feed something that a human or an application queries. And that final hop — from "stream flowing into a system" to "query returning a result" — has its own class of technology and its own class of tradeoffs, distinct from everything Phase H covered before. This module is that final hop.

// THE PROBLEM · CONTINUOUS STREAM IN · SUB-SECOND QUERIES OUT · ARBITRARY AGGREGATIONS
CDC · KAFKA · FLINK STREAMS 100K events/sec continuous ingest REAL-TIME ANALYTICS Materialize · Pinot · Druid SELECT region, SUM(revenue)... SELECT user, COUNT(*) FROM ... SELECT hour, AVG(latency)... continuous ingest + sub-second queries query DASHBOARDS live · sub-second refresh: 200ms users: 10K concurrent // SPECIFIC TENSION Continuous ingest requires fast writes · arbitrary queries require fast reads Streaming state requires memory · historical depth requires disk Traditional systems optimize for one. Real-time analytics platforms are built to hit both.
The problem shape is specific: continuous ingest of 100K+ events/sec, arbitrary SQL-shaped queries returning in under a second, dashboards refreshing multiple times per second for thousands of concurrent viewers. Postgres can query fast but ingests slowly; Snowflake queries fast but ingests in minutes-hours; Kafka ingests fast but doesn't do queries. Real-time analytics platforms are the class of system built precisely to close this gap — either by continuously maintaining query results as the input changes (streaming SQL), or by writing columnar segments optimized for ingestion + aggregation (real-time OLAP).

The specific difficulty is that "fast writes" and "fast reads" are usually in tension. Systems optimized for OLTP writes (Postgres, MySQL) use row-oriented storage with B-tree indexes that are cheap to update but expensive to scan for analytics. Systems optimized for OLAP reads (Snowflake, BigQuery, Redshift) use columnar storage with heavy compression and pre-computed aggregates that are cheap to query but expensive to update — the whole ingestion pipeline is batch-oriented, taking minutes to hours to load new data. Real-time analytics platforms sit in the gap: they accept sub-second ingest of individual events (like OLTP) while also serving sub-second aggregation queries over billions of rows (like OLAP). The two families of solutions to this gap take radically different approaches, and understanding when to use each is the specific engineering intuition M.43 is trying to build.

// FOUR ATTEMPTS AT "REAL-TIME ANALYTICS ON STREAMING DATA" · WHAT EACH GETS WRONG
Attempt 1: query Postgres directly// SELECT ... FROM events WHERE ...
"Just point the dashboard at the production Postgres. It's already got the data." Works for very small tables (under 10M rows) and low query rates. Falls apart quickly: analytical queries are full-scans that hurt OLTP performance; the dashboard puts read load on the primary; index bloat from analytical query patterns; storage grows unboundedly as events accumulate. And Postgres can't do the heavy lifting real analytics needs — complex GROUP BYs over 100M rows, window functions across time, dimensional cube queries. Every serious "we started with Postgres" data platform migrates off it before scaling. Anti-pattern §05.i is specifically about this failure mode.// FAIL MODE: hurts OLTP + query performance ceiling + storage growth
HURTS
OLTP
Attempt 2: nightly ETL to Snowflake// dbt models refreshed nightly
"Extract data nightly into Snowflake/BigQuery; dashboards query the warehouse." Solves the query performance problem — warehouses are excellent at analytical SQL. Also solves the OLTP isolation problem — production is untouched. But the freshness is completely wrong for real-time use cases: dashboards show data that's up to 24 hours stale. Fraud detection needs sub-second freshness; live inventory dashboards need seconds-fresh data; user-facing analytics need "up-to-the-second" semantics. Batch ETL to warehouses simply can't provide these — the whole pipeline (extract, transform, load, refresh materialized views) takes minutes to hours. Correct for BI dashboards showing yesterday's revenue; wrong for anything user-facing today.// FAIL MODE: 24h freshness · unusable for real-time
24h
STALE
Attempt 3: in-memory cache with periodic refresh// Redis cached query results, refreshed every 60s
"Cache aggregation query results in Redis; refresh from source database every 60 seconds; dashboard reads from Redis." Solves the query latency (Redis is fast) and reduces DB load. But the fundamental problem is the refresh cycle: any change to source data isn't visible until the next refresh, so events between refreshes are effectively invisible. Aggregations lag by up to the refresh interval. And the "refresh from source" step is a full re-computation of every cached query — expensive at scale, doesn't scale with cache size. The refresh cycle IS the problem: any system that periodically recomputes analytics from scratch has a latency ceiling equal to the refresh period. What we actually want is incremental maintenance: only update what changed. That's what real-time analytics platforms provide.// FAIL MODE: refresh cycle = latency floor · full recomputation
REFRESH
CYCLE
Attempt 4: real-time analytics platform// Materialize (streaming SQL) or Pinot/Druid (real-time OLAP)
"Ingest from Kafka continuously; incrementally maintain query results OR pre-aggregated columnar segments; dashboards query results directly." Two flavors. Streaming SQL (Materialize, RisingWave): you write a SELECT query; the system continuously maintains its result via incremental view maintenance. As inputs change, only the affected output rows change; queries against the maintained view return in microseconds. Real-time OLAP (Pinot, Druid): events are ingested into columnar segments with pre-computed aggregations; queries hit the segments with heavy indexing (bitmap indices, star trees). Both approaches achieve the "sub-second ingest + sub-second query" property that batch systems can't. Which one fits depends on the query workload — see §04.// FIT: sub-second ingest + sub-second queries
PRODUCTION
GRADE
// THE COMPOSITE PATTERN

Each earlier attempt fails at either freshness (batch is too slow), isolation (Postgres-direct hurts OLTP), or incrementality (refresh cycles are the wrong shape). Real-time analytics platforms are the specific class of system that hits all three simultaneously — continuously ingesting events, incrementally maintaining query results or columnar aggregations, and serving arbitrary queries at sub-second latencies. The two families (streaming SQL vs real-time OLAP) trade off differently, but both close the gap in ways batch systems fundamentally can't.

The historical arc tells a specific story. Real-time OLAP came first, from ad-tech scale problems around 2011-2013: Metamarkets built Druid to serve real-time advertising analytics; LinkedIn built Pinot for the same class of internal dashboards; both were open-sourced around 2014-2015. Both took the "columnar + pre-aggregation + streaming ingest" approach that made ad-tech dashboards viable. Streaming SQL emerged separately, from a different theoretical foundation: Frank McSherry's differential dataflow work at Microsoft Research (published 2013) formalized incremental view maintenance in a general form; Materialize (2019) built a production system on it; RisingWave (2021) followed with a Rust reimplementation. The two families didn't converge from the same starting point — they're solutions to overlapping problems from different intellectual traditions. The workload types they excel at reflect their origins: real-time OLAP wins on high-cardinality dashboards and time-series aggregation (its ad-tech heritage); streaming SQL wins on complex joins and transactional consistency (its dataflow heritage). Understanding this history helps understand why the tools have different strengths — they were solving different problems first, and only later got compared as alternatives.

Sub-second ingest and sub-second queries used to be in tension. Real-time analytics platforms broke the tension by refusing to periodically recompute — everything is incrementally maintained.
§ 02 — Two families · two mental models

Streaming SQL,
or real-time OLAP.

The two families of real-time analytics platforms take radically different approaches to the same class of problem. Getting the distinction right is the first architectural decision: pick the wrong family for your workload and you'll fight the system's grain for the entire deployment. The two families aren't competitors that do the same thing at different quality — they're different tools optimized for different query shapes, with overlap in the middle. The choice depends on what your queries actually look like.

// FAMILY A · STREAMING SQL
Keep a query result up-to-date.

Mental model: "I write a SELECT query. The system maintains its result continuously as inputs change. When I ask for the answer, I get it immediately — it's already computed."

How it works: Incremental View Maintenance via differential dataflow. Input changes propagate through the dataflow graph; only affected output rows change. The result of a JOIN is maintained rather than recomputed.

Wins at: Complex multi-table joins, transactional consistency, small-to-medium result sets, SQL-native semantics. Great for dashboards where the query IS the definition of what you want to see.

Loses at: Very high cardinality group-bys, TB-scale historical depth, ad-hoc queries over huge time ranges (materialized views are precomputed for specific queries).

Materialize RisingWave ksqlDB Flink SQL
// FAMILY B · REAL-TIME OLAP
Make aggregations fast to query.

Mental model: "Events land in columnar segments with pre-computed aggregations. Queries hit the segments with heavy indexing — bitmaps, star trees, bloom filters. Any dimensional slice returns fast."

How it works: Ingestion writes to columnar segments (Parquet-like); segments are indexed for fast filter and aggregation; queries scatter across segments and gather results.

Wins at: High-cardinality dashboards (millions of dimensions), time-series aggregations (roll-up-heavy), ad-hoc slicing across huge event streams. Great for "count/sum/percentile over a big table filtered by many dimensions."

Loses at: Complex joins (limited support or slow), transactional consistency (eventual only), stream state that requires ordered processing (Materialize's territory).

Apache Pinot Apache Druid ClickHouse StarRocks

The mental model difference is the important part. In streaming SQL, you write the query first and the system figures out how to maintain it incrementally — your artifact is the SQL statement. In real-time OLAP, you configure how data is ingested (partitioning, rollups, indices) and then queries hit the columnar store — your artifact is the schema and ingestion config. These are two different engineering skills. A team fluent in one is often clumsy with the other on their first project. Being deliberate about which family you're choosing, and why, is what separates senior real-time analytics engineering from cargo-culting.

The middle ground and boundary cases matter too. ClickHouse straddles the boundary — technically columnar OLAP but with strong SQL support including some joins; often used where teams want "one system that handles both" and are willing to trade optimality for versatility. Flink SQL sits between streaming SQL and stream processing — you can write SELECT queries against streaming inputs, and it can emit results to sinks, but it doesn't maintain a queryable materialized view the way Materialize does. Snowflake and BigQuery are strictly batch (minutes-to-hours ingestion latency) but with strong SQL and joins — they're the batch competitor to streaming SQL, not to real-time OLAP. Placing a new tool on this map takes ~10 minutes of reading the docs: look at ingestion latency, query latency, join support, and cardinality ceiling; each metric points toward one family or the other.

The end-to-end pattern that closes Phase H is worth naming explicitly: Postgres → Debezium (M.42) → Kafka (M.39) → Flink (M.40, M.41) → Materialize or Pinot or Druid (M.43) → user-facing dashboards or APIs. Every hop in this pipeline has been covered by a specific module. The specific insight is that this pipeline is not a single system — it's five independently-scalable, independently-operable components with well-defined interfaces between them. Each does one thing well; the composition is what makes it viable. The alternative — building all of this into a single monolithic "analytics database" — has been tried repeatedly and always fails, because the specialization at each hop is what makes each hop tractable. The multi-component pipeline IS the modern real-time analytics architecture. M.43's final answer to "how do we do real-time analytics" is: build this pipeline, and use the right analytics engine for the query shape at the end.

Streaming SQL keeps queries up-to-date. Real-time OLAP makes queries fast to compute. Both close the sub-second-ingest-with-sub-second-query gap; each closes it for a different query shape.
§ 03 — Incremental view maintenance · the mechanism

Don't recompute.
Update in place.

The specific mechanism that makes streaming SQL work — and the one worth understanding in depth because it maps to the general "how do we do continuous computation efficiently?" problem — is incremental view maintenance (IVM). The idea: given a query and a stream of input changes, compute how the query's output changes without recomputing the entire query. Classical databases have supported materialized views for decades, but refreshing them was periodic and expensive — full recomputation of the query on a schedule. IVM makes the maintenance continuous and proportional to the change size: an input change that affects one row of the output causes exactly one row of the output to be updated, in microseconds. This is the specific property that Materialize built its business on and RisingWave replicated.

// INCREMENTAL VIEW MAINTENANCE · DATAFLOW GRAPH · CHANGE PROPAGATION

SELECT region, SUM(revenue) FROM orders JOIN users USING(user_id) GROUP BY region orders stream CDC from Postgres +INSERT +UPDATE -DELETE users stream CDC from Postgres users.region column DATAFLOW GRAPH · differential operators JOIN user_id GROUP BY region Δ input +orders.new_row Δ output region_X: +$50 one input change (+1 order row) → one output change (+revenue for region_X) work proportional to Δ, not to |data| MATERIALIZED VIEW revenue_by_region US-WEST: $12,450 US-EAST: $18,220 EU-CENTRAL: $9,880 APAC: $7,340 POINT QUERY SELECT * FROM revenue_by_region ~microseconds key insight: the JOIN result is maintained continuously — not recomputed on query queries against the view are point reads · O(1) latency regardless of input size
The mechanism. The user's SQL query is compiled into a dataflow graph — a DAG of operators (JOIN, GROUP BY, FILTER, PROJECT) with data flowing through edges. Input CDC changes enter as differential updates (+row for INSERT, -row for DELETE, or a pair for UPDATE). Each operator processes the change locally: JOIN produces the incremental join contribution; GROUP BY updates the affected aggregate. The final operator's output is stored as the materialized view. Work is proportional to the change, not to the total input size — an INSERT that adds one order row causes one update to one region's aggregate, taking microseconds, regardless of whether the orders table has 1M or 1B rows. Point queries against the view are O(1) key lookups.
i
Differential updates.

Input changes represented as (row, +1) for INSERT and (row, -1) for DELETE; UPDATE is (old, -1) then (new, +1). This algebra makes every operator's response to changes uniform: apply the delta to the local state, propagate the resulting delta downstream. Frank McSherry's differential dataflow (2013) is the theoretical foundation.

ii
Dataflow graph.

SQL query compiles to a DAG of operators. JOIN, GROUP BY, FILTER, PROJECT, UNION are all differential operators — each knows how to translate an input delta into an output delta. The graph is fixed at query-registration time; deltas flow through it continuously as inputs change.

iii
Operator state.

Some operators (JOIN, GROUP BY) maintain internal state to handle deltas correctly. The JOIN operator maintains both input relations indexed by join key so that a new left-row can be matched against all existing right-rows. State is checkpointed to durable storage — the exact discipline from M.41.

iv
Point queries.

The final output of the dataflow graph is stored as a queryable relation. SELECT * FROM view is a point read against the materialized state — microseconds, regardless of the underlying query's complexity. This is the property that makes streaming SQL viable for user-facing dashboards.

The work-proportional-to-change property is the specific reason streaming SQL platforms can maintain complex queries continuously without falling behind. A traditional database materialized view refresh recomputes the whole query — an ANALYZE command over billions of rows. An IVM-maintained view processes only the rows that changed since last update. For workloads where input change rate is much smaller than total data size (which is nearly every production workload), the two are orders of magnitude apart in cost. A view that would take 20 minutes to refresh in Postgres via REFRESH MATERIALIZED VIEW is maintained continuously in Materialize with sub-second update latency and no scheduled refresh at all. The tradeoff is that you commit to specific queries at view-definition time — arbitrary ad-hoc queries aren't supported in the same way; only the queries you've defined as views get the IVM treatment.

The connection to Flink and M.41 is worth naming. Flink itself supports streaming SQL — Flink SQL — using its own stream-processing engine as the substrate. The mechanism is similar (SQL compiles to a dataflow graph; state is checkpointed via the same discipline from M.41), but Flink's model is "process streams and emit to sinks" rather than "maintain a queryable materialized view." Materialize's specific innovation was making the maintained view itself the primary interface: your applications query the view directly (via a Postgres-compatible SQL interface), rather than receiving events emitted by the streaming system. This shifts the mental model from "stream processing that also happens to query" to "database that also happens to stream." Both work; the choice depends on whether your consumers want to poll a query or subscribe to a change stream. Materialize won the dashboard use case; Flink SQL is more commonly used when the output feeds another streaming system.

Work proportional to change, not to data size. This is the specific property that makes continuous maintenance viable at scale. Everything else is engineering around this invariant.
§ 04 — Real-time analytics explorer

Three systems.
Three workloads.

Below: each of the three canonical real-time analytics systems (Materialize · Pinot · Druid) evaluated against three different workload profiles (complex SQL joins · high-cardinality dashboards · time-series aggregations). Watch the diagonal — each system has one workload it was built for, and one it will actively fight you on. The 9 combinations illustrate why "which real-time analytics system?" is a decision about query shape, not vendor preference.

RTAP.SIM // m.43 lab
Workload profile →
// SYSTEM BEHAVIOR · under current workload
// METRICS · OPERATIONAL PROFILE
Ingest latency-
Query latency (p99)-
Join support-
Cardinality ceiling-
Historical depth-
Overall fit-
// VERDICT
Loading...
...
§ 05 — Where real-time analytics decisions decay

Silent
freshness bugs.

The failure modes of real-time analytics platforms are especially treacherous because the platform silently degrades before it visibly breaks. Queries return results that are ~30 seconds stale instead of ~500ms; ingestion falls behind by minutes without an alert; the dashboard shows numbers that seem plausible but aren't correct. Diagnosing these requires understanding the specific failure modes each family has — and building the monitoring discipline that catches them before users do.

// FIVE REAL-TIME ANALYTICS ANTI-PATTERNS

i
The Postgres as analytics DB
"Our production Postgres is our analytics store. The dashboards query it directly. Now it's the bottleneck for both OLTP and analytics — writes are slow and dashboards are laggy."

Postgres was designed for OLTP: row-oriented storage, B-tree indexes for point queries, MVCC for concurrent transactions. These properties are actively bad for analytics: analytical queries do full-scans that saturate the buffer pool, evicting OLTP working set; complex GROUP BYs hold long transactions that block VACUUM and cause table bloat; dashboards put steady read load on the primary. The fix: separate the analytics workload onto a dedicated system. For real-time needs, that's a streaming pipeline (M.42 CDC → Kafka → Materialize/Pinot/Druid); for batch needs, an EL pipeline to Snowflake/BigQuery. Every serious data platform migrates off Postgres-as-analytics before scaling — the only question is whether you migrate proactively or after an outage.

ii
The ignored late-arriving data
"Our Materialize view shows total revenue by hour. Yesterday's 3pm number is different today than it was at 4pm yesterday. Users are asking why the 'immutable historical' number is changing."

Real-time analytics platforms process events as they arrive, which means late-arriving events (M.40's watermark territory) modify historical aggregates. A revenue event that gets stuck in a client-side buffer and arrives 24 hours late will retroactively update the aggregate for its actual event-time hour. This is correct behavior — the aggregate should include all events — but it's confusing to users who see "yesterday's revenue" change today. The fix: (a) allowed-lateness discipline (M.40) — configure the view to reject events beyond N hours late; (b) sealed-window semantics — mark historical windows as immutable after a grace period; (c) UI communication — show "revenue as of <timestamp> · may include late events" instead of a static number. Late-arriving data is a real-time-analytics-specific application of M.40's watermark and lateness discipline.

iii
The cardinality explosion
"Our Druid queries have gotten 100× slower over 6 months. Investigation shows the user_id dimension has 50M unique values, and every dashboard groups by it. Segment metadata is exploding."

Real-time OLAP systems pre-compute indices for each dimension; high-cardinality dimensions (user_id, session_id, request_id — millions to billions of unique values) blow up index size and merge cost. Druid's dictionary encoding, which is efficient for low-cardinality dimensions, becomes memory-prohibitive for high-cardinality ones. The fix: (a) don't put high-cardinality columns as dimensions — treat them as metric filters instead; (b) if you need per-user aggregation, pre-aggregate upstream (Flink can do this) and store per-user only for hot users, using cardinality-aware storage (HyperLogLog for count-distinct, T-Digest for percentiles); (c) or move that specific workload to Materialize/streaming-SQL, which handles high-cardinality group-by more gracefully via IVM (proportional to change, not to cardinality). Cardinality is a first-class OLAP tuning knob — treating it as an afterthought is anti-pattern §05.iii's specific failure mode.

iv
The no pre-aggregation strategy
"Our Pinot cluster is running 40 nodes and we're serving 500 QPS. The team is now proposing to double the cluster for the next quarter's growth. Nobody wants to look at the query plans."

Real-time OLAP platforms are fast because they've done the aggregation work in advance — rollups at ingest time, pre-computed indices, star trees for common query patterns. When teams don't invest in this pre-aggregation strategy, queries scan raw events at query time; cost scales linearly with data volume, not with query complexity. The solution isn't more hardware; it's understanding which queries are hot and building appropriate rollups and indices for them. The fix: (a) profile query patterns — the top 10 queries are usually 90% of load; (b) design ingestion rollups for those specific patterns (Druid's granularity: HOUR pre-aggregates events per hour at ingest); (c) build star trees or bloom filters (Pinot) for common filter dimensions; (d) accept that some ad-hoc queries will be slow, and route them to a separate cluster or a batch warehouse. Pre-aggregation is a design decision, not an accident; failing to design it is what makes clusters grow indefinitely.

v
The no query-time governance
"A junior analyst wrote a dashboard that runs a full-cross-product query without WHERE clauses. It took down the Druid cluster for 40 minutes. We had no way to catch it before it deployed."

Real-time analytics platforms are shared infrastructure — one bad query can affect every dashboard. Without query-time governance, a single unbounded query, cross-join, or high-cardinality group-by can consume the whole cluster's resources. The failure mode is that queries which look reasonable in isolation are catastrophic in aggregate — no filter on a time column means scanning years of data; no LIMIT means returning millions of rows; joins without indexed keys mean CROSS JOINs. The fix: (a) query cost limits at the platform level (max scan bytes, max rows returned, max execution time); (b) mandatory query review for new dashboards and BI reports; (c) per-user or per-team quotas so one team can't monopolize; (d) query classification (interactive vs batch) with different resource pools. Query governance is the operational discipline that keeps real-time analytics platforms operationally viable as user counts grow — every mature deployment builds it in as a hard requirement, not as a "we'll add it later."

The composite pattern across all five is that real-time analytics operational discipline is different from batch analytics discipline. Batch systems (Snowflake, BigQuery) have well-understood cost models — per-query billing, autoscaling, mature query optimizers. Real-time analytics platforms are cluster-based (you provision N nodes), workload-sensitive (one bad query hurts the whole cluster), and design-dependent (pre-aggregation strategies determine performance). Mature real-time analytics deployments invest in ingestion design, query governance, and workload isolation from day one; deployments that skip these three discover their absence through outages.

Phase H ends here. The five modules — M.39 (batch vs stream), M.40 (windowing), M.41 (stateful streams), M.42 (CDC), M.43 (real-time analytics) — cover the end-to-end streaming discipline: how to think about latency, how to handle event time, how to persist state, how to source from operational databases, how to make the resulting streams user-facing. The composite skill is what mature data platform engineers spend years building. Every module you've completed is a specific piece of that skill; the Phase H completion banner below marks the composite.

Real-time analytics is not just "faster batch analytics." It's a different discipline with different failure modes, different design decisions, and different operational realities. Recognize that or fight the platform.
§ 06 — Eight words for the real-time conversation

Vocabulary,
for the last hop.

The terms that show up in every "should we use Materialize or Pinot?" architecture review, every "why is our Druid query slow?" postmortem, every "should we pre-aggregate at ingest?" design doc.

Incremental View Maintenance
/ˌɪn.krəˈmɛn.tl vjuː ˈmeɪn.tən.əns/
The technique of updating a materialized view's output as inputs change, without recomputing the whole query. Work is proportional to the change size, not the total data size. The specific mechanism that makes streaming SQL viable at scale. Materialize's business is built on this.
Differential Dataflow
/ˌdɪf.əˈrɛn.ʃəl ˈdeɪ.tə.floʊ/
Frank McSherry's 2013 theoretical framework for incremental computation over dataflow graphs. Represents changes as (row, +1) / (row, -1) deltas; each operator (JOIN, GROUP BY) processes deltas locally. The theoretical foundation Materialize is built on.
Materialize
/məˈtɪr.i.ə.laɪz/
Open-source streaming SQL platform, Postgres-compatible. You write CREATE MATERIALIZED VIEW ... AS SELECT ...; the system maintains the result incrementally as CDC inputs change. Point queries against the view return in microseconds. Founded 2019, based on differential dataflow.
Real-Time OLAP
/ˈriːl taɪm ˈoʊ.læp/
Columnar analytics engines that support streaming ingestion + sub-second aggregation queries. Distinguished from batch OLAP (Snowflake, BigQuery) by ingest latency (seconds vs minutes-hours). Canonical examples: Apache Pinot, Apache Druid, ClickHouse.
Rollup (ingestion)
/ˈroʊl.ʌp/
Pre-aggregation applied during ingestion. Druid's granularity: HOUR config means events within the same hour get aggregated on ingest — one row per hour instead of one per event. Trades detail (can't see individual events) for query speed (drastically less data to scan). Anti-pattern §05.iv is the failure to design this.
Segment
/ˈsɛɡ.mənt/
Druid/Pinot's unit of storage — a self-contained columnar file with its own indices. Segments are immutable once written; new events go into new segments; old segments get merged periodically. Queries scatter across segments in parallel and gather results.
Star Tree Index
/stɑːr triː/
Pinot's specific index that pre-computes aggregations along common dimension combinations. For queries filtering on any subset of the indexed dimensions, the star tree answers directly without scanning raw rows. Makes "show me count by region + product + hour" instant. The specific mechanism that makes Pinot fast on dashboards.
Late-arriving data
/leɪt əˈraɪ.vɪŋ/
Events that arrive after their event-time window would suggest they should have. Real-time analytics on late-arriving data retroactively modifies historical aggregates — correct semantically but confusing to users. M.40's watermark and allowed-lateness discipline apply here directly.
§ 07 — Knowledge check

Five questions.
The last-hop intuition.

Test the vocabulary. Click an answer; explanation drops in instantly.

QUESTION 1 OF 5
Loading question...
Score: 0 / 5
5 / 5

Analytics, closed.

Perfect. Incremental view maintenance, columnar OLAP, rollups, star trees, late-arriving data — the vocabulary that makes real-time analytics deployments viable at multi-year scale. Phase H is closed; Phase I (capstones) is next.

§ 08 — The recap

Three ideas to
carry forward.

The discipline that makes real-time analytics platforms trustworthy at production scale.

i

Two families, different origins

Streaming SQL (Materialize, RisingWave) maintains query results incrementally via differential dataflow — you write SELECT, the system keeps it up-to-date. Real-time OLAP (Pinot, Druid) does columnar storage with heavy pre-aggregation and streaming ingestion. Different intellectual traditions, different query-shape strengths. Picking the wrong family means fighting the system's grain.

ii

Work proportional to change

Incremental view maintenance is what makes streaming SQL scale. An input change affecting one row of output causes exactly one row of output to update — regardless of underlying data size. Materialize maintains complex JOIN + GROUP BY views in microseconds after each input delta. Traditional materialized-view refreshes were periodic and expensive; IVM makes maintenance continuous and cheap.

iii

Operational discipline is different

Real-time analytics platforms are cluster-based, workload-sensitive, and design-dependent. Pre-aggregation strategies, query governance, cardinality management, workload isolation — these are day-one requirements, not "we'll add them later." Mature deployments invest in them from the start; skipping them produces the anti-pattern failure modes in §05.

◈ PHASE MILESTONE

Phase H.
Complete.

Five modules on streams and data pipelines: batching latency, event-time semantics, stateful processing, sourcing from operational databases, real-time analytics. The composite is the modern streaming discipline — enough to design, build, and operate a production streaming platform end-to-end.

// MODULES · M.39 → M.43
M.39
Batch vs Stream

The latency spectrum. When to use which.

M.40
Windowing

Tumbling, sliding, session. Event time, watermarks, lateness.

M.41
Stateful Streams

Checkpoints, barriers, state backends, exactly-once.

M.42
Change Data Capture

Streams sourced from operational databases. Debezium, WAL, replication slots.

M.43
Real-Time Analytics

The last hop. Streaming SQL vs real-time OLAP. Dashboards.

// WHAT PHASE H BUILT · THE COMPOSITE SKILL

Phase H's arc is a single narrative. M.39 established that streaming is a latency band, not a technology — you use it when the latency requirement makes batch unviable. M.40 established that event-time semantics are non-negotiable at scale — clocks disagree, events arrive late, and pretending otherwise produces silently-wrong results. M.41 established that operational state has to be checkpointed properly if the pipeline is going to survive real failures — Chandy-Lamport's algorithm, adapted for streaming, is what makes exactly-once semantics possible. M.42 established that streams have to come from somewhere — usually a relational database, and log-based CDC via Debezium is the mechanism that satisfies the four requirements (complete, ordered, low-impact, recoverable). M.43 established that the streams have to end somewhere too — either as continuously-maintained materialized views (Materialize) or as columnar segments queryable at sub-second latencies (Pinot, Druid).

The composite pipelinePostgres → Debezium → Kafka → Flink → Materialize/Pinot/Druid → dashboards — is the modern streaming data platform. Every hop is specialized; every hop has its own operational discipline; the composition is what makes it viable. Teams that get one hop wrong (skimping on CDC, ignoring watermarks, using the wrong state backend, choosing the wrong analytics family) build fragile pipelines that break in ways that are hard to diagnose. Teams that build all five hops with the discipline from Phase H build streaming platforms that survive years of operational reality.

This is the mainstream production shape as of 2026, and understanding each hop deeply is what senior data platform engineering looks like today.

NEXT · PHASE I Intermediate Capstones. M.44 (Distributed K-V Store) · M.45 (Real-Time Analytics Platform) · M.46 (Intermediate Interview Toolkit). Three modules where all the phase-level discipline gets composed into end-to-end system-design exercises.
↓ UP NEXT · PHASE I OPENER

M.44 — Distributed
K-V store.

The first Intermediate capstone. Design a distributed key-value store from scratch. Consistent hashing, replication, quorum reads/writes, vector clocks, anti-entropy. The composite of every distributed-systems pattern from Phase E, applied end-to-end.

Continue to Module 44 →