Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 47

Streaming to Delta Lake with Checkpointing

Recommended resources

Hello! Welcome back to our series on Structured Streaming.

In the last lesson, we took the first crucial step in building a streaming pipeline: creating a streaming DataFrame using spark.readStream. You learned how to ingest data from both cloud files with Auto Loader and from Delta tables, effectively defining the source of your stream.

Today, we complete the picture. We will learn how to take that streaming DataFrame and write its results to a destination, known as a sink. Our focus will be on the most important sink in the Databricks ecosystem: a Delta table. By the end of this lesson, you will be able to write streaming query results to a Delta table sink using spark.writeStream with checkpointing, creating your first end-to-end, fault-tolerant streaming pipeline.

The Anatomy of a Streaming Write

Just as spark.readStream is the entry point for reading a stream, DataFrame.writeStream is the starting point for writing one. It allows you to define the sink, the output mode, and other crucial configurations for your stream.

The general syntax pattern in PySpark is:

streaming_query = (streaming_df
  .writeStream
  .format("delta")
  .outputMode("append")
  .option("checkpointLocation", "/path/to/checkpoint")
  .toTable("my_target_table")
)

Let's break down these components:

  • .writeStream: This signals that you are defining a streaming sink for your DataFrame.
  • .format("delta"): Specifies that you're writing to a Delta table.
  • .outputMode("append"): This is one of the most important settings. append mode means that only new rows processed since the last trigger will be added to the sink table. It's the default and most common mode for simple ETL streams. We'll cover other modes in a future lesson.
  • .option("checkpointLocation", ...): This is a mandatory option for most production-grade streams. It specifies a path where Spark will store the progress of the stream.
  • .toTable("..."): This is a convenient method to start the stream and write the output to a table registered in the Unity Catalog or Hive Metastore. You can also use .start("/path/to/table") to write to a specific file path.

The Heart of Reliability: Checkpointing

Before we write any code, we must understand checkpointing. It is the single most important concept for ensuring your streaming queries are reliable and fault-tolerant.

A checkpoint is a directory on cloud storage where Spark records the state of your stream. This state includes:

  1. Offsets: A record of which data has been read from the source (e.g., which files have been processed, what was the last offset read from Kafka).
  2. Stateful Operator Data: For more complex streams with aggregations, this is where the intermediate state is stored.

This checkpoint acts as the stream's memory.

Databricks Structured Streaming Architecture with Checkpointing
This diagram shows the role of checkpointing in a fault-tolerant streaming engine. The engine uses checkpoints and write-ahead logs to recover from failures and ensure data is processed exactly once.

Why is this so critical?

  • Fault Tolerance: If your stream stops for any reason (cluster failure, manual stop, etc.), the checkpoint allows it to restart from exactly where it left off, without missing any data or reprocessing old data.
  • Exactly-Once Guarantee: By meticulously tracking what's been processed, checkpointing, combined with a transactional sink like Delta Lake, provides an end-to-end exactly-once processing guarantee.

The following video explains the concept and the syntax for checkpointLocation.

Spark Structured Streaming with PySpark | DP-700 Exam Prep

In this video from 'Aleksi Partanen - Master Microsoft Fabric', the presenter introduces the syntax for writeStream and clearly explains the role of the checkpoint location.

Please watch two short clips: 06:16 to 07:02: Focus on the explanation of checkpointLocation and its role in error handling and tracking processed data. 18:22 to 19:27: This part explains what Spark stores inside the checkpoint folder and emphasizes why you should not delete it.

A crucial rule to remember is: Every streaming query must have its own unique checkpoint directory. Sharing a checkpoint directory between two different streams will lead to incorrect results and failures.


Writing a Stream to a Delta Sink

Now that we understand the importance of checkpointing, let's see the code in action. The official Databricks documentation provides a clear, minimal example of the syntax.

Delta table streaming reads and writes | Databricks on AWS

This documentation page provides code snippets for writing to a Delta table. We'll focus on the default 'Append mode'.

Review the Python code example under the 'Append mode' section. Notice the clean structure: writeStream, outputMode, checkpointLocation, and toTable.

This next video provides a great walkthrough of a full read-and-write stream, demonstrating how the pieces fit together.

Advancing Spark - Databricks Delta Streaming

This video from Advancing Analytics shows a live demo of data being written to a Delta sink.

Please watch from 03:12 to 05:59. The first part walks through the writeStream code, and the second part shows the stream running live, picking up new rows as they are inserted into the source.


Your Turn: Build a Complete Streaming Pipeline

It's time for you to write the code. We'll build on the Auto Loader example from our last lesson to create a complete pipeline that reads from files and writes to a Delta table.

Goal: Create a streaming pipeline that reads CSV files with Auto Loader and writes the data to a Delta table.

  1. Set Up Your Notebook: In a new Databricks notebook, run the following code to prepare a source directory and drop a sample CSV file into it.

    
    
    
    # Define paths for the source data, the target table, and the checkpoint
    source_path = "/FileStore/streaming_sink_input"
    table_name = "streaming_output_table"
    checkpoint_path = f"/tmp/delta/{table_name}/_checkpoint"
    
    
    
    
    # Clean up previous runs
    dbutils.fs.rm(source_path, recurse=True)
    dbutils.fs.rm(checkpoint_path, recurse=True)
    spark.sql(f"DROP TABLE IF EXISTS {table_name}")
    
    
    
    
    # Create the source directory
    dbutils.fs.mkdirs(source_path)
    
    
    
    
    # Create a sample CSV file
    csv_data = """
    transaction_id,item,amount
    1,ItemA,19.99
    2,ItemB,25.00
    """
    dbutils.fs.put(f"{source_path}/data_1.csv", csv_data, overwrite=True)
    print("Setup complete. A sample CSV file has been created.")
    
  2. Define the Streaming Read: This is the readStream code from our previous lesson.

    
    
    
    # Configure the Auto Loader stream to read the CSV files
    streaming_df = (spark.readStream
      .format("cloudFiles")
      .option("cloudFiles.format", "csv")
      .option("header", "true")
      .option("cloudFiles.inferColumnTypes", "true")
      .option("cloudFiles.schemaLocation", checkpoint_path) # Auto Loader uses the same path for schema and source tracking
      .load(source_path)
    )
    
  3. Define and Start the Streaming Write: Now, it's your turn. Complete the code below to write streaming_df to the table_name defined in step 1. You must specify the output mode and the checkpoint location.

    
    
    
    # YOUR CODE HERE
    # Complete the writeStream operation
    streaming_query = (streaming_df
      .writeStream
      .format("delta")
      .outputMode("append") # Use append mode to add new records
      .option("checkpointLocation", checkpoint_path) # Use the same checkpoint path
      .toTable(table_name)
    )
    

    Hint: You need to chain methods like .format(), .outputMode(), .option(), and .toTable().

  4. Verify the Output: Once the stream is running, open a new cell and query your target table. You should see the two records from data_1.csv.

    -- In a new SQL cell
    SELECT * FROM streaming_output_table;
    
  5. Test the End-to-End Stream: While the stream is still active, run the following code in another cell to drop a new file into the source directory.

    
    
    
    # Add a new file to the source directory
    new_csv_data = """
    transaction_id,item,amount
    3,ItemC,10.50
    4,ItemD,55.10
    """
    dbutils.fs.put(f"{source_path}/data_2.csv", new_csv_data, overwrite=True)
    

    Now, re-run the SELECT query from step 4. You should see the two new rows appear in your table automatically. Congratulations, you've built a working, end-to-end streaming pipeline!


Understanding Checkpoint Failure and Recovery

To truly appreciate the power of checkpointing, let's see what happens when it's gone.

Advancing Spark - Databricks Delta Streaming

The 'Advancing Spark' video demonstrates exactly what happens when you delete a checkpoint and restart a stream. This is a powerful lesson in why checkpoints are essential.

Watch from 14:06 to 15:07. Observe how deleting the checkpoint directory causes the stream to reset and reprocess all data from the beginning, leading to duplicate records in the sink.

After watching the video, you can stop the streaming query in your notebook (click the "Cancel" button on the cell). If you were to re-run the cell containing the writeStream code, it would pick up from where it left off. But if you were to delete the checkpoint_path directory first and then restart, it would re-process data_1.csv and data_2.csv, adding duplicate rows to your streaming_output_table. This is why protecting your checkpoint directory is so important.


Knowledge Check

Let's test your understanding with a couple of quick questions.

Question 1:
You are configuring a streaming write. Which option is used to specify the directory for storing the stream's state and progress?
A. checkpointDirectory
B. checkpointPath
C. checkpointLocation
D. checkpoint

Click to see the answer **C. `checkpointLocation`** is the correct option name used in `spark.writeStream`.

Question 2:
You are building a pipeline to ingest files from cloud storage. To ensure that files are processed exactly once, even if the stream restarts after a failure, which of the following is most critical?
A. Using .outputMode("append").
B. Setting .trigger(availableNow=True).
C. Specifying a unique and persistent checkpointLocation.
D. Using .format("delta") for the sink.

Click to see the answer **C. Specifying a unique and persistent `checkpointLocation`**. While the other options are part of a streaming pipeline, the checkpoint is the mechanism that tracks which files have been processed, making it the most critical component for exactly-once processing across restarts.

Conclusion

In this lesson, you have completed the fundamental workflow of a Structured Streaming job. You now have the skills to not only read streaming data but also to write it reliably to a Delta table, the foundation of the Lakehouse architecture.

Key Takeaways

  • You use DataFrame.writeStream to write a streaming DataFrame to a sink.
  • The format("delta") specifies that the sink is a Delta table, which provides transactional guarantees.
  • Checkpointing is mandatory for reliable streaming. You must provide a unique path using the .option("checkpointLocation", ...) for each stream.
  • The checkpoint allows a stream to recover from failures and ensures exactly-once processing by remembering what data it has already processed.
  • The .toTable() method is a convenient way to start a stream and register its output as a table in the metastore.

Next Steps

We've used the default append output mode in this lesson. However, different use cases require different behaviors. For example, how would you write the results of a streaming aggregation where the entire result needs to be updated with each batch?

In our next lesson, we will explore this by learning to select the appropriate output mode (append, complete, update) for a given streaming scenario.