<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/ba-p/151728</link>
    <description>&lt;H1&gt;How to: Master Streaming Tables and Materialized Views&lt;/H1&gt;
&lt;P&gt;Make sure to check out the previous post: &lt;A href="https://community.databricks.com/t5/technical-blog/spark-declarative-pipelines-how-to-series-part-1-how-to-save/ba-p/149180" target="_blank" rel="noopener"&gt;https://community.databricks.com/t5/technical-blog/spark-declarative-pipelines-how-to-series-part-1-how-to-save/ba-p/149180&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;Welcome to the second post of the &lt;STRONG&gt;Lakeflow Spark Declarative Pipelines (SDP) “How-To” Series&lt;/STRONG&gt;! In the previous post, we saw how SDP lets us focus entirely on business logic by automating persistence, checkpoints, and state management.&lt;/P&gt;
&lt;P&gt;I can hear you asking:&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;"Okay, I get it now. I don’t have to call an explicit &lt;/EM&gt;&lt;CODE&gt;&lt;EM&gt;.save()&lt;/EM&gt;&lt;/CODE&gt;&lt;EM&gt;. My code is cleaner, my logic is decoupled from the plumbing, and I’m finally thinking declaratively. But now I’m looking at these decorators (&lt;/EM&gt;&lt;CODE&gt;&lt;EM&gt;@​​​dp.table&lt;/EM&gt;&lt;/CODE&gt;&lt;EM&gt; and &lt;/EM&gt;&lt;CODE&gt;&lt;EM&gt;@​​​dp.materialized_view&lt;/EM&gt;&lt;/CODE&gt;),&amp;nbsp;&lt;EM&gt;and they both seem to create tables. Wait, there’s another one, &lt;/EM&gt;&lt;CODE&gt;&lt;EM&gt;@​​​dp.temporary_view&lt;/EM&gt;&lt;/CODE&gt;&lt;EM&gt;? Which one do I use for my Bronze layer? &lt;I&gt;&lt;SPAN&gt;Which one is for the Gold layer&lt;/SPAN&gt;&lt;/I&gt;?"&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;In this post, we’ll dive into these building blocks and show how to choose the right one for a specific use case.&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;The Basis: It’s all about the Flow&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;In the declarative world, we don't just create tables; we define the flow that hydrates them. Think of a flow as a "data contract" between your source and destination.&lt;/P&gt;
&lt;P&gt;A flow determines two things:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Which&lt;/STRONG&gt; data is moving (defined by a query or a DataFrame).&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;How&lt;/STRONG&gt; it lands (Append, Upsert, or Replace).&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;A flow is not just a query - it is a stateful, incremental computation with zero plumbing. It tracks what has already been processed, so that on each run only new or changed data is handled. This is what allows incremental semantics, whether you run it in streaming or batch mode.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;In SDP, flows are the core of your pipeline. Sometimes they are defined implicitly through a decorator, and sometimes you define them explicitly to handle more complex logic.&lt;/P&gt;
&lt;P&gt;Finally, each flow has its own destination - a physical table in Unity Catalog (UC), or a sink to an external system.&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;Virtual Logic: The &lt;/STRONG&gt;&lt;CODE&gt;&lt;STRONG&gt;@​​​dp.temporary_view&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/H3&gt;
&lt;P&gt;Before we persist anything, let's talk about &lt;STRONG&gt;Views&lt;/STRONG&gt;. Not every step in your pipeline needs to be a physical table.&lt;/P&gt;
&lt;P&gt;The &lt;CODE&gt;@​​​dp.temporary_view&lt;/CODE&gt; decorator allows you to define temporary views. These are perfect for structuring your code and making it more modular and readable. They don't cost anything in storage and are ephemeral. You can easily reference them within your pipeline. A view lives within its pipeline only. Temporary views are useful when you want to break a pipeline into smaller transformation steps without creating extra physical tables. The results are then persisted downstream into Streaming Tables or Materialized Views.&lt;/P&gt;
&lt;P&gt;Here's how you'd typically create a reusable transformation. Notice how the SDP version wraps the same logic in a decorator, turning it into a named, referenceable pipeline object:&lt;/P&gt;
&lt;TABLE border="1" width="100%" cellspacing="0" cellpadding="6"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TH style="font-size: 1.2em;" width="50%"&gt;&lt;FONT size="3"&gt;&lt;STRONG&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Spark Imperative Code&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TH&gt;
&lt;TH style="font-size: 1.2em;" width="50%"&gt;&lt;FONT size="3"&gt;&lt;STRONG&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Spark Declarative Pipelines&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TH&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD width="50%" style="vertical-align: top;"&gt;&lt;LI-CODE lang="python"&gt;cleaned_sales_logs = (
  spark.readStream.table("raw_logs")
    .filter("amount &amp;gt; 0")
    .withColumnRenamed("ts", "event_timestamp")
)
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;TD width="50%"&gt;&lt;LI-CODE lang="python"&gt;@​dp.temporary_view
def cleaned_sales_logs():
    return (
     spark.readStream.table("raw_logs")
        .filter("amount &amp;gt; 0")
        .withColumnRenamed("ts", "event_timestamp")
    )
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;H3&gt;&lt;STRONG&gt;The Append Flow&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;Imagine you have a Kafka topic or a directory of raw JSON files. Your goal is to land them into your Bronze layer as quickly as possible, perhaps with light, row-by-row transformations, like column formatting, adding a &lt;CODE&gt;CASE WHEN&lt;/CODE&gt; statement, or joining with a static reference table. This is the classic &lt;STRONG&gt;Append Flow&lt;/STRONG&gt;. Because the Append Flow is stateful, it automatically tracks which data has already been processed. Only new rows are ingested on each run, and no manual bookkeeping required.&lt;/P&gt;
&lt;P&gt;A &lt;STRONG&gt;Streaming Table&lt;/STRONG&gt; (&lt;CODE&gt;@​​​dp.table&lt;/CODE&gt;) is the primary target for an Append flow.&lt;/P&gt;
&lt;P&gt;This is the direct equivalent of the Spark Structured Streaming &lt;STRONG&gt;writeStream&lt;/STRONG&gt;&lt;STRONG&gt; API&lt;/STRONG&gt; with &lt;CODE&gt;.outputMode("append")&lt;/CODE&gt;.&lt;/P&gt;
&lt;P&gt;When you use the &lt;CODE&gt;@​​​dp.table&lt;/CODE&gt; decorator, SDP implicitly creates an Append Flow for you:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;@​​dp.table
def bronze_orders():
    return spark.readStream.table("raw_kafka_orders")&lt;/LI-CODE&gt;
&lt;P&gt;However, you can also define the flow &lt;STRONG&gt;explicitly&lt;/STRONG&gt; if you want more control over the source-to-target relationship.&amp;nbsp;&lt;SPAN&gt;This is helpful when you have multiple flows targeting the same Streaming Table:&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# First, declare the target destination
dp.create_streaming_table("bronze_orders") 

# Then, explicitly define the flow 1 that hydrates it 
@dp.append_flow(target="bronze_orders")
def flow_cleaned_sales_logs():
    # This refers to our .temporary_view above!
    return dp.read_stream("cleaned_sales_logs")

# Define the schema for Kafka JSON messages
# order_schema = StructType([StructField("order_id", StringType()), ...])

# Define a second flow that hydrates from a Kafka topic
@dp.append_flow(target="bronze_orders")
def append_kafka_orders():
   return (
       spark.readStream
       .format("kafka")
       .option("kafka.bootstrap.servers", "&amp;lt;broker-host&amp;gt;:9092")
       .option("subscribe", "orders_topic")
       .option("startingOffsets", "latest")
       .load()
       .select(F.from_json(F.col("value").cast("string"), order_schema).alias("data"))
       .select("data.*")
   )
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;What if your Append Flow needs to write to an external database or Kafka topic instead of a table?&amp;nbsp; For arbitrary sinks, there are the &lt;CODE&gt;dp.create_sink()&lt;/CODE&gt; function and the &lt;CODE&gt;@​dp.foreach_batch_sink()&lt;/CODE&gt; decorator. We will deep dive into custom sinks in a future post.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;The Append Once Flow&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;I know what you're thinking: &lt;EM&gt;"I really like this streaming approach, but I can't move to it. All my old data is sitting in a legacy table, and my Kafka topic retention is only 7 days. If I start a stream now, I lose my history. How do I bridge the gap?"&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;This is the perfect use case for the &lt;STRONG&gt;Append Once Flow&lt;/STRONG&gt;. You can use a one-time batch flow to hydrate your table with legacy data, and then let the Append flow take over for the fresh data.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# 1. Declare the target table
dp.create_streaming_table("bronze_orders")

# 2. THE BACKFILL: Fill the table once from your legacy Parquet data
@​dp.append_flow(target="bronze_orders", once=True)
def backfill_historical():
    return (
         spark.read.format("parquet")
         .load("/Volumes/catalog/schema/historical_data")
    )

# 3. THE STREAM: Let the real-time flow handle everything moving forward
@​dp.append_flow(target="bronze_orders")
def stream_cleaned_sales_logs():
    return spark.readStream.table("cleaned_sales_logs")&lt;/LI-CODE&gt;
&lt;H4&gt;&lt;STRONG&gt;The AUTO CDC Flow&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;What if your source isn't just a list of events, but a stream of database changes? You have updates and deletes, and you need your target table to reflect the &lt;STRONG&gt;current state&lt;/STRONG&gt;. This is the classic Slowly Changing Dimension (SCD) Type 1 or Type 2 scenario.&lt;/P&gt;
&lt;P&gt;In the traditional approach, this meant writing a massive &lt;CODE&gt;MERGE INTO&lt;/CODE&gt; block inside a &lt;CODE&gt;foreachBatch&lt;/CODE&gt;. Worse, you had to repeat that same boilerplate for every single pipeline you built.&lt;/P&gt;
&lt;P&gt;In SDP, we replace that with the &lt;STRONG&gt;AUTO CDC Flow&lt;/STRONG&gt;. And what is the physical target of an AUTO CDC Flow? It's the Streaming Table.&lt;/P&gt;
&lt;P&gt;To use it, you first declare your target as a streaming table, and then use the function &lt;CODE&gt;dp.create_auto_cdc_flow()&lt;/CODE&gt;.&lt;/P&gt;
&lt;TABLE border="1" width="100%" cellspacing="0" cellpadding="6"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TH style="font-size: 1.2em;" width="50%" height="33px"&gt;&lt;FONT size="3"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Spark Imperative code&lt;/FONT&gt;&lt;/TH&gt;
&lt;TH style="font-size: 1.2em;" width="50%" height="33px"&gt;&lt;FONT size="3"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Spark Declarative Pipelines&lt;/FONT&gt;&lt;/TH&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD width="50%" height="765px"&gt;&lt;LI-CODE lang="python"&gt;from pyspark.sql import DataFrame
from pyspark.sql.functions import col, struct, max_by
def upsert_to_delta(micro_batch_df, batch_id):
    # Deduplicate within the micro-batch to get the latest record per key
    (micro_batch_df
     .groupBy("key")
     .agg(max_by(struct("*"),
          col("ts")).alias("row"))
     .select("row.*")
     .createOrReplaceTempView("updates"))
    # Execute the MERGE logic
    # Note: Replace the ellipses with your actual column mapping
    spark.sql("""
        MERGE INTO cdc_data_raw t
        USING updates s
        ON s.key = t.key
        WHEN MATCHED AND s.is_deleted = true THEN 
            UPDATE SET DELETED_AT = now()
        WHEN MATCHED THEN UPDATE SET 
            A = CASE WHEN s.ts &amp;gt; t.ts THEN s.a ELSE t.a END,
            B = CASE WHEN s.ts &amp;gt; t.ts THEN s.b ELSE t.b END,
            -- ... for every column ...
            ts = CASE WHEN s.ts &amp;gt; t.ts THEN s.ts ELSE t.ts END
        WHEN NOT MATCHED THEN INSERT *
    """)
# Start the stream
(cdcData.writeStream
    .foreachBatch(upsert_to_delta)
    .outputMode("append")
    .start())
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;TD width="50%" height="765px" style="vertical-align: top;"&gt;&lt;LI-CODE lang="python"&gt;from pyspark import pipelines as dp
from pyspark.sql.functions import col, expr
@​dp.view
def users():
    return (
          spark.readStream
         .table("cdc_data.users")
        )
dp.create_streaming_table("target")
dp.create_auto_cdc_flow(
    target = "target",
    source = "users",
    keys = ["key"],
    sequence_by = col("ts"),
    apply_as_deletes = expr("is_deleted = True")
)
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;H3&gt;&lt;STRONG&gt;Materialized Views&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;Once your data is in Silver, you usually want to aggregate it for Gold in order to calculate daily revenue or active users. This is where &lt;STRONG&gt;Materialized Views&lt;/STRONG&gt; (&lt;CODE&gt;@​dp.materialized_view&lt;/CODE&gt;)&amp;nbsp;&lt;SPAN&gt;come into the picture.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;A Materialized View is a special pipeline object which stores results of a query.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;Think of a Materialized View as a query acceleration mechanism. It persists the result of your query and &lt;STRONG&gt;automatically updates&lt;/STRONG&gt; it incrementally whenever the source changes. T&lt;SPAN&gt;he framework &lt;/SPAN&gt;appends, upserts, and deletes in order to reflect the current truth.&lt;/P&gt;
&lt;P&gt;To solve this in traditional ETL, you usually have to choose between two difficult paths:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;The Recompute Path:&lt;/STRONG&gt; Whenever an update happens, you re-run the aggregation for that entire chunk of data (like a whole day or month). This gets more complex when handling &lt;STRONG&gt;late-arriving data&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;The Watermark Path:&lt;/STRONG&gt; You use watermarks to define a time window. The tradeoff? Any data arriving after the watermark is dropped and never considered.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Materialized Views solve this by acting as a &lt;STRONG&gt;"State" Mirror&lt;/STRONG&gt;. They persist the query result and automatically update it incrementally. If data arrives late, SDP knows how to revisit that state and update the summary without you writing a single line of merge logic:&lt;/P&gt;
&lt;TABLE border="1" width="100%" cellspacing="0" cellpadding="6"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TH style="font-size: 1.2em;" width="50%"&gt;&lt;FONT size="3"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Spark Imperative Code&lt;/FONT&gt;&lt;/TH&gt;
&lt;TH style="font-size: 1.2em;" width="50%"&gt;&lt;FONT size="3"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Spark Declarative Pipelines&lt;/FONT&gt;&lt;/TH&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD width="50%"&gt;&lt;LI-CODE lang="python"&gt;def update_aggs(microBatchDF, batchId):
    # 1. Identify affected dates
    distinct_dates = microBatchDF
.select("order_date").distinct().collect()
    date_list = [row.order_date for row in distinct_dates]
    
    if len(date_list) &amp;gt; 0:
        # 2. Recalculate chunks from source
        new_aggs = (spark.read
            .table("silver_orders")
            .filter(F.col("order_date")
                    .isin(date_list))
            .groupBy("order_date")
            .agg(F.sum("amount")
                 .alias("total_revenue")))
# 3. Sink: Merge into target
new_aggs.createOrReplaceTempView("updates")
spark.sql("""
    MERGE INTO daily_sales t 
    USING updates s ON s.order_date = t.order_date
    WHEN MATCHED THEN UPDATE SET t.total_revenue = s.total_revenue
     WHEN NOT MATCHED THEN INSERT *
     """)
(spark.readStream.table("silver_orders").writeStream.foreachBatch(update_aggregates).start())
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;TD width="50%" style="vertical-align: top;"&gt;&lt;LI-CODE lang="python"&gt;@​dp.materialized_view
def daily_sales():
    return (spark.read
       .table("silver_orders")
       .groupBy("order_date")
       .agg(F.sum("amount").alias("total_revenue")))
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;Let's clarify the difference. You might be wondering: &lt;EM&gt;"Why can't I just use an Append Flow and a Streaming Table for my aggregations?"&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;An &lt;STRONG&gt;Append Flow&lt;/STRONG&gt; never looks back on what has already been processed. It only moves forward, appending new rows as they arrive. Because it never revisits past data, running a standard &lt;CODE&gt;SUM&lt;/CODE&gt; or &lt;CODE&gt;GROUP BY&lt;/CODE&gt; over an &lt;STRONG&gt;Append Flow&lt;/STRONG&gt; would require the engine to maintain an ever-growing state of all previously seen rows, with no natural point at which to finalize the calculation.&lt;/P&gt;
&lt;P&gt;To do aggregations, we need a &lt;STRONG&gt;finite table -&amp;nbsp;&lt;/STRONG&gt;a specific "state" or "snapshot" that represents the current truth. &lt;STRONG&gt;This is exactly why we use &lt;/STRONG&gt;&lt;CODE&gt;&lt;STRONG&gt;spark.read&lt;/STRONG&gt;&lt;/CODE&gt;&lt;STRONG&gt; instead of &lt;/STRONG&gt;&lt;CODE&gt;&lt;STRONG&gt;spark.readStream&lt;/STRONG&gt;&lt;/CODE&gt;&lt;STRONG&gt; inside a Materialized View.&lt;/STRONG&gt; By using &lt;CODE&gt;read&lt;/CODE&gt;, we tell SDP to treat the source as a full dataset to be queried, which the framework then intelligently optimizes to update incrementally behind the scenes.&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;Final Decision Tree&lt;/STRONG&gt;&lt;/H3&gt;
&lt;TABLE style="border-collapse: collapse; font-size: 1em;" border="1" width="100%" cellspacing="0"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TH style="padding: 12px 16px; font-size: 1.15em; text-align: left;" width="45%"&gt;&lt;FONT size="3"&gt;If you need...&lt;/FONT&gt;&lt;/TH&gt;
&lt;TH style="padding: 12px 16px; font-size: 1.15em; text-align: left;" width="30%"&gt;&lt;FONT size="3"&gt;Use this&lt;/FONT&gt;&lt;/TH&gt;
&lt;TH style="padding: 12px 16px; font-size: 1.15em; text-align: left;" width="25%"&gt;&lt;FONT size="3"&gt;Flow Type&lt;/FONT&gt;&lt;/TH&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;To land raw data from a streaming append-only source&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;&lt;CODE&gt;&lt;STRONG&gt;@​dp.table&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;Append Flow&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;To process CDC data&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;&lt;CODE&gt;&lt;STRONG&gt;dp.create_auto_cdc_flow&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;AUTO CDC Flow&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;A one-time backfill&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;&lt;CODE&gt;&lt;STRONG&gt;@​​dp.append_flow(once=True)&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;Append Once Flow&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;Aggregations / Current Truth&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;&lt;CODE&gt;&lt;STRONG&gt;@​​​dp.materialized_view&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;N/A*&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;Code refactoring&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;&lt;CODE&gt;&lt;STRONG&gt;@​​​dp.temporary_view&lt;/STRONG&gt;&lt;/CODE&gt;&lt;/TD&gt;
&lt;TD style="padding: 10px 16px; vertical-align: middle;"&gt;N/A*&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;EM&gt;* Remember, &lt;/EM&gt;&lt;EM&gt;Materialized Views and temporary views are pipeline objects rather than flows. Flows define how data moves into a destination, while these objects define how data is stored or structured within the pipeline.&lt;/EM&gt;&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;Key Takeaway&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;Flows are the core primitive of SDP: stateful, incremental computations that track what has been processed, so you only need to decide how data lands, and the framework handles the rest. Streaming Tables and Materialized Views are targets of the flows. Altogether, they intelligently handle technical complexity, allowing you to focus entirely on the business logic.&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;Conclusion&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;Today, we covered how Flows land data into Streaming Tables and how Materialized Views help accelerate queries. In our next post, we’ll look at the &lt;STRONG&gt;Consumer&lt;/STRONG&gt; side: how to read from these objects, the nuances between &lt;CODE&gt;spark.read&lt;/CODE&gt; and &lt;CODE&gt;spark.readStream&lt;/CODE&gt;, and how to choose the right one for your specific scenario. Stay tuned!&lt;/P&gt;</description>
    <pubDate>Fri, 17 Apr 2026 13:15:43 GMT</pubDate>
    <dc:creator>aleksandra_ch</dc:creator>
    <dc:date>2026-04-17T13:15:43Z</dc:date>
    <item>
      <title>SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views</title>
      <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/ba-p/151728</link>
      <description>&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Gemini_Generated_Image_9ejwob9ejwob9ejw.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/24275i72A055CFA248C22D/image-size/large?v=v2&amp;amp;px=999" role="button" title="Gemini_Generated_Image_9ejwob9ejwob9ejw.png" alt="Gemini_Generated_Image_9ejwob9ejwob9ejw.png" /&gt;&lt;/span&gt;&lt;/P&gt;</description>
      <pubDate>Fri, 17 Apr 2026 13:15:43 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/ba-p/151728</guid>
      <dc:creator>aleksandra_ch</dc:creator>
      <dc:date>2026-04-17T13:15:43Z</dc:date>
    </item>
    <item>
      <title>Re: SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views</title>
      <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/152223#M966</link>
      <description>&lt;P&gt;Nice summary!&lt;/P&gt;</description>
      <pubDate>Thu, 26 Mar 2026 21:06:13 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/152223#M966</guid>
      <dc:creator>Hubert-Dudek</dc:creator>
      <dc:date>2026-03-26T21:06:13Z</dc:date>
    </item>
    <item>
      <title>Re: SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views</title>
      <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/153862#M988</link>
      <description>&lt;P&gt;Really enjoyed this breakdown especially how you positioned streaming tables for ingestion and materialized views for transformation. It makes designing real-time pipelines much more intuitive for SQL users.&lt;/P&gt;&lt;P&gt;One question: in large-scale environments with thousands of tables, how do you decide when to rely purely on streaming tables vs introducing materialized views, considering both cost and latency trade offs?&lt;/P&gt;</description>
      <pubDate>Thu, 09 Apr 2026 10:03:24 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/153862#M988</guid>
      <dc:creator>antoalphi</dc:creator>
      <dc:date>2026-04-09T10:03:24Z</dc:date>
    </item>
    <item>
      <title>Re: SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views</title>
      <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/153908#M990</link>
      <description>&lt;P&gt;Hi&amp;nbsp;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/181633"&gt;@antoalphi&lt;/a&gt;&amp;nbsp;,&lt;/P&gt;
&lt;P&gt;Thanks for the feedback!&lt;/P&gt;
&lt;P&gt;The choice between &lt;STRONG data-index-in-node="44" data-path-to-node="9"&gt;Streaming Tables (ST)&lt;/STRONG&gt; and &lt;STRONG data-index-in-node="70" data-path-to-node="9"&gt;Materialized Views (MV)&lt;/STRONG&gt; really comes down to your required processing semantics:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;If a row from the source needs to be processed only once / the logic never looks back at historical data -&amp;gt; go for a Streaming Table&lt;/LI&gt;
&lt;LI&gt;If your logic requires complex aggregations, joins, or updates to existing records -&amp;gt;&amp;nbsp; you have to go for a Materialized View&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;For example, Streaming Table is typically not suitable for aggregations and joins, just because the semantics need to look back to existing records in the table in order to perform those. There are exceptions which I plan to discuss in my next post.&lt;/P&gt;
&lt;P&gt;If you need to ingest thousands of sources into UC tables in an incremental way with lightweight row-by-row processing - Streaming Tables is a good choice.&lt;/P&gt;
&lt;P&gt;If you have to create aggregations / joins on those tables - you have to go with Materialized Views on top.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;All in all, wherever you have thousands of tables or just few of them - the choice really depends on the business logic.&lt;/P&gt;
&lt;P&gt;A note regarding large-scale environments:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;One pipeline can contain up to 1000 datasets (including Materialized Views, Streaming Tables, and Temporary Views). At a large scale, pipeline organization is critical since all datasets within a single pipeline share the same compute resources and lifecycle. If one table hangs, it can impact the entire group.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;If you have a specific pipeline diagram or a list of requirements, feel free to share it. I’d be happy to provide a more tailored vision for the setup!&lt;/P&gt;</description>
      <pubDate>Thu, 09 Apr 2026 12:35:05 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/153908#M990</guid>
      <dc:creator>aleksandra_ch</dc:creator>
      <dc:date>2026-04-09T12:35:05Z</dc:date>
    </item>
    <item>
      <title>Re: SDP “How-To” Series. Part 2: How to Master Streaming Tables and Materialized Views</title>
      <link>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/156982#M1065</link>
      <description>&lt;P&gt;I just want to double down that these articles are super helpful so the time put into them is well worth it! Especially for data engineers and architects supporting databricks.&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Fri, 15 May 2026 12:17:53 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/sdp-how-to-series-part-2-how-to-master-streaming-tables-and/bc-p/156982#M1065</guid>
      <dc:creator>Randy_meacham</dc:creator>
      <dc:date>2026-05-15T12:17:53Z</dc:date>
    </item>
  </channel>
</rss>

