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
๐Ÿ“Š
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 tableMaterialized view
ProcessingRow once (incremental)Refreshes results
LatencyLowHigher
Dim changesMay miss late updatesAlways current
Pick forAppend ingestCorrect 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

๐Ÿ–ฑ๏ธ 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

Common gotchas