Reading and writing Delta - batch and streaming

Estimated read: 14 min

Every Delta table you’ll touch in this guide can be read and written either as a static batch snapshot or as a continuously-updating stream – using nearly identical code either way. This lecture covers both, and the practical question of when each fits.

Batch reads and writes

# Batch read: a snapshot as of right now
df = spark.read.format("delta").table("main.default.orders")

# Batch write
(df.write
   .format("delta")
   .mode("append")
   .saveAsTable("main.default.orders"))

This is what you already expect: read a table, get back exactly what’s committed at the moment you read it, regardless of what happens afterward. Every example in the previous two lectures was this batch model.

Streaming reads: a table as a continuous feed

stream_df = (spark.readStream
             .format("delta")
             .table("main.default.orders"))

The only syntactic difference from a batch read is readStream instead of read – but the semantics change completely. Instead of a fixed snapshot, stream_df represents new rows as they’re committed to the source table, processed incrementally rather than reprocessing everything on each run.

(stream_df.writeStream
    .format("delta")
    .outputMode("append")
    .option("checkpointLocation", "/Volumes/main/checkpoints/orders_silver/")
    .trigger(availableNow=True)
    .table("main.default.orders_silver"))

Key term: the checkpoint location is where Spark Structured Streaming tracks exactly which input has already been processed – restart the stream and it resumes from there, rather than reprocessing from the beginning or losing track entirely. This is the mechanism that makes streaming exactly-once in practice, and it’s specific to this stream/sink pairing – reusing a checkpoint location across unrelated streams will produce wrong results.

Trigger modes: how “streaming” actually runs

Structured Streaming doesn’t require an always-on process – trigger controls the cadence:

Trigger Behavior
trigger(availableNow=True) Process everything currently available, then stop – effectively “incremental batch”
trigger(processingTime="5 minutes") Wake up every 5 minutes, process what’s new, repeat
Continuous (no trigger specified) Process new data as it arrives, near-continuously

For most data engineering pipelines in this guide – including everything in Part 2’s StepRight project – availableNow=True on a scheduled job (via Lakeflow Jobs, Section 10) is the practical default: you get streaming’s incremental processing semantics without paying for an always-on cluster.

Why use the streaming API for batch-shaped work at all?

This is the part worth sitting with if you’re coming from a warehouse world where batch and streaming were entirely separate tools. The streaming API’s real value here isn’t “near real-time” – it’s automatic incremental processing: Spark tracks what’s already been read via the checkpoint, so each run only processes genuinely new data, without you hand-writing watermark or high-water-mark logic the way you likely did in a legacy incremental-load ETL job.

Legacy incremental load:
  1. Query max(updated_at) already processed
  2. WHERE updated_at > last_max_watermark
  3. Manually track and persist the new watermark

Delta streaming read + checkpoint:
  1. readStream, process, write
  2. Checkpoint tracks progress automatically
  3. Rerun -- only new data processes, no manual watermark logic

Batch vs. streaming: which for which

Use batch when Use streaming (with availableNow) when
One-off analysis or backfill Regular incremental pipeline runs
Full-table transformations that need the whole dataset You want automatic new-data-only processing
Small, infrequently-updated reference/dimension tables High-volume fact tables updated continuously

You’ll see this exact choice made explicitly in Section 8 (Lakeflow Connect) and Section 9 (Lakeflow Declarative Pipelines), where streaming tables are the primary building block for bronze and silver layers specifically because of this incremental-by-default behavior.


| ← Previous: Creating and Managing Delta tables | Next: Time Travel - querying historical versions → | |:—|—:|


Back to top

Independent, self-authored Databricks companion notes.

This site uses Just the Docs, a documentation theme for Jekyll.