Databricks DE ยท Topic 5.2
Streaming Tables, MVs, Flows & Sinks
The building blocks of an SDP pipeline โ streaming tables for incremental ingest, materialized views for always-correct results, flows that feed them (and fan-in), and sinks that write out. This deep dive walks each with the code.
ST vs MVappend flowsexternal sinkswatermarks
The one-liner
Streaming table = incremental Delta table (row processed once). Materialized view = cached results that refresh. Flow = the query that feeds an ST/sink (multiple flows can fan into one ST). Sink = external target (Kafka/Event Hubs/Delta).
๐ฅ Analogy: a streaming table is a security camera recording โ keeps appending new footage, never re-films the past. A materialized view is a live scoreboard โ recomputes totals whenever the scores change so it's always correct.
The four objects โ click each
โธ streaming table ยท materialized view ยท flow ยท sink
Streaming table โ a Delta table with incremental/streaming support; each input row is processed once (append-mostly). Ideal for ingestion. Owned/updated by a single pipeline.
Materialized view โ cached precomputed query results that refresh when sources change. Keeps joins/aggregations always correct.
Flow โ the query (DataFrame API) that processes data into a streaming table or sink. One ST can have multiple append flows (fan-in many sources).
Sink โ an external destination a flow writes to: Apache Kafka, Azure Event Hubs, or a Delta/UC table. Python-only, streaming-only.
1 ยท Streaming table or materialized view? โ click to reveal
Pick the right object for each scenario:
High-volume clickstream ingest from cloud storage โ Streaming table (append-only, low latency)
Daily revenue joined to a dimension that changes โ Materialized view (always reflects current dims)
Each input row should be processed exactly once โ Streaming table
Correctness of an aggregation matters more than latency โ Materialized view
| Streaming table | Materialized view |
| Processing | Row once (incremental) | Refreshes results |
| Latency | Low | Higher |
| Dim changes | May miss late updates | Always current |
| Pick for | Append ingest | Correct joins/aggs |
Python (current API) & SQL
from pyspark import pipelines as dp
@dp.table # streaming table โ incremental ingest
def bronze_events():
return spark.readStream.format("cloudFiles").option("cloudFiles.format","json").load(src)
@dp.materialized_view # MV โ always-correct join to current dim
def user_activity():
return spark.read.table("bronze_events").join(spark.read.table("dim_users"), "user_id")
CREATE OR REFRESH STREAMING TABLE bronze_events
AS SELECT * FROM STREAM read_files('/Volumes/main/raw/events', format => 'json');
CREATE OR REFRESH MATERIALIZED VIEW user_activity
AS SELECT e.*, u.segment FROM bronze_events e JOIN dim_users u USING (user_id);
2 ยท Flows โ fan many sources into one ST
Create the streaming table explicitly, then attach append flows โ each processes incrementally and independently, feeding one ST (cleaner than a giant UNION that reprocesses everything).
dp.create_streaming_table("all_events") # target ST (no query of its own)
@dp.append_flow(target="all_events") # flow 1: US
def us_events(): return spark.readStream.table("bronze_events_us")
@dp.append_flow(target="all_events") # flow 2: EU โ same target
def eu_events(): return spark.readStream.table("bronze_events_eu")
3 ยท Sinks โ write out to Kafka / Event Hubs / Delta
Create a sink with create_sink, then point an append flow at it. Sinks are Python-only and accept streaming queries only.
dp.create_sink(
name="events_kafka",
format="kafka", # also: "delta", Event Hubs (via Kafka), custom
options={"kafka.bootstrap.servers": "host:9092", "topic": "enriched_events"})
@dp.append_flow(target="events_kafka") # write the stream to the sink
def publish_events():
return spark.readStream.table("user_activity").selectExpr(
"CAST(user_id AS STRING) AS key", "to_json(struct(*)) AS value")
4 ยท Stateful streams need watermarks
Streaming aggregations/joins keep state. Without a watermark bounding how late data can arrive, that state grows unbounded โ OOM.
@dp.table
def events_per_min():
from pyspark.sql.functions import window, col
return (spark.readStream.table("bronze_events")
.withWatermark("event_ts", "10 minutes") # bound late data โ bounded state
.groupBy(window(col("event_ts"), "1 minute")).count())
Why it matters
- ST vs MV is the #1 SDP interview question โ pick wrong and you reprocess everything (slow) or serve stale joins (wrong).
- Flows + sinks show SDP is more than two table types โ fan multiple inputs into one ST and write out to Kafka.
๐ฑ๏ธ Real-world use case: bronze_events (ST) ingests clickstream incrementally; dim_users changes daily so user_activity is an MV re-joining to current users; an append flow unions a second region, and a sink writes enriched events to Kafka.
Uses, edge cases & limitations
- Uses: STs for bronze/silver ingest; MVs for gold aggregates & dimension-joined tables; append flows to fan many sources into one ST; sinks to push results out.
- Edge cases: STs are append-mostly โ schema changes/reprocessing often need a full refresh; unbounded streaming aggs/joins need watermarks or state grows unbounded; an ST is owned by a single pipeline.
- Limitation: MV refresh has latency/cost; STs can be "fast-but-wrong" if a dimension changed after a row was processed (so joins often belong in MVs). Sinks: Python-only, streaming-only.
Common gotchas
- โ Streaming table for a dimension join that must stay current โ use an MV.
- โ Materialized view for high-volume append ingest โ use a streaming table.
- โ Trying to define a sink in SQL โ sinks are Python-only, streaming-only.
- โ Forgetting watermarks on unbounded streams โ unbounded state / OOM.
- โ Writing to one streaming table from two pipelines โ conflicts (single owner).