CertSafari
    Databricks Certified Data Engineer Professional· Lessons

    Domain 5 · Lesson 30/46

    Streaming Change Data Feed for Incremental Downstream Processing

    Apply Change Data Feed (CDF) to expose row-level changes (updates/deletes) for efficient incremental downstream processing.

    10 min read
    2.5% of exam
    2 sources
    Published 3 Oct 2026
    Docs as of 30 Sep 2026

    What you will be able to do

    • Explain why streaming from a table's change data feed is the recommended way to propagate updates and deletes downstream
    • Compare change data feed with skipChangeCommits, full refresh and materialized views
    • Configure a readChangeFeed stream, including the initial snapshot, startingVersion and rate limits
    • Plan for change data feed retention and recover a CDF stream after a corrupted checkpoint

    1.Why plain streaming breaks on updates and deletes

    A plain Delta streaming source only works on append-only tables. Once the source table takes UPDATE, DELETE, MERGE INTO or OVERWRITE, you have four options, and only one of them carries every change type downstream.

    skipChangeCommits keeps the stream alive by ignoring every file-changing commit, so updates and deletes never reach the target. The older ignoreChanges re-emits rewritten files, which means duplicates, and still drops deletes. A full refresh is simple but you pay for the whole dataset every time. Materialized views propagate changes for you, but with higher latency, and they exist only in Lakeflow pipelines and Databricks SQL. The change data feed carries inserts, updates and deletes downstream as explicit events. The cost is that you write logic to handle each _change_type.

    Handling upstream changes to a Delta streaming source
    ApproachPropagates updates/deletes?Main cost
    skipChangeCommitsNo; only processes appendsChanges need separate logic
    Full refreshYes, by reprocessing everythingExpensive for large datasets; all downstream tables reprocess
    Change data feedYes; inserts, updates, and deletesMore complex logic per change type
    Materialized viewsYes; automatic change propagationHigher latency; Lakeflow pipelines and Databricks SQL only

    Checkpoint 1 of 6· Check yourself

    A GDPR process deletes and updates rows in a source Delta table. A downstream silver table must reflect those deletes incrementally, without reprocessing the full source. Which approach fits?

    Sources12

    2.Configuring a readChangeFeed stream

    Streaming is the recommended way to consume a change feed, and it is the only way Databricks tracks which versions you have already processed. With a batch table_changes call, keeping track of versions is your job. A stream's checkpoint records it for you.

    Stream a table's change data feedpython
    (spark.readStream
      .option("readChangeFeed", "true")
      .table("myTable")
    )

    That initial snapshot is right for a new pipeline whose target is empty. If the target already holds everything up to some version, pass startingVersion (or a timestamp) so the stream doesn't load the whole source state again as INSERT events.

    The usual rate limits, maxFilesPerTrigger and maxBytesPerTrigger, plus excludeRegex, work on change data. After the initial snapshot, limits apply to whole commits. A micro-batch takes all of a commit or defers it to the next batch, so a large MERGE is never split across batches.

    Checkpoint 2 of 6· Check yourself

    A target table already contains every change from a source up to version 500. You start a new readChangeFeed stream into it. Which setting avoids reprocessing the source's full state?

    Checkpoint 3 of 6· Exam question

    A downstream job must audit exactly the row-level changes that occurred between Delta version 12 and version 18, inclusive, on `analytics.orders`, which has Change Data Feed enabled. Which SQL statement retrieves precisely this range of changes?

    Sources2

    3.Retention, permanent history, and checkpoint recovery

    The change feed is not an archive. It only holds changes made after it was enabled, and its records are transient. When table versions are cleaned up, their change data goes with them. With legacy CDF, VACUUM deletes the change data files, and changes computed from the transaction log follow the checkpoint retention policy. Once a version is gone you can't read its changes any more. If you need a permanent change history, read the feed incrementally and write it to a table of its own.

    For that kind of audit or replay job you rarely need an always-on cluster. An availableNow trigger processes everything available and then stops, so it can run on a schedule:

    Persist the change feed to a target table as an incremental batchpython
    (spark.readStream
      .option("readChangeFeed", "true")
      .table("source_table")
      .writeStream
      .option("checkpointLocation", <checkpoint-path>)
      .trigger(availableNow=True)
      .toTable("target_table")
    )

    Checkpoint 4 of 6· Fill the gap

    Complete the recovery stream so it picks up right after the last processed version

    (spark.readStream
      .option("readChangeFeed", "true")
      .option(" ? ", 76)
      .table("source_table")
      .writeStream
      .option("checkpointLocation", "<new-checkpoint-path>")
      .toTable("target_table")
    )

    Checkpoint 5 of 6· Check yourself

    Compliance wants every change to a table kept for seven years. Which design meets that?

    Checkpoint 6 of 6· Exam question

    A team reads Change Data Feed output from `finance.ledger` and writes Python logic that branches on the `_change_type` column to route rows to an audit table. Which statements about the `_change_type` values are correct?(Select 3)

    Sources2

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Once CDF is enabled, the table keeps a permanent, queryable record of every change ever made.Why is that wrong?

      The feed only covers changes since enablement, and versions expire. For permanent history, write the feed into its own table.

      Covered in Retention, permanent history, and checkpoint recovery

    2. 2.skipChangeCommits lets a stream pass upstream updates and deletes through to the target.Why is that wrong?

      skipChangeCommits drops every commit that rewrites files, so only appends get through. Carrying changes downstream needs CDF or separate logic.

      Covered in Why plain streaming breaks on updates and deletes

    3. 3.A new readChangeFeed stream only emits commits made after it starts.Why is that wrong?

      Without a starting version, the first batch is the whole current table emitted as INSERT records. Set startingVersion when the target already has that state.

      Covered in Configuring a readChangeFeed stream

    Practise it for real

    Create a CDF-enabled table, make inserts, an update and a delete, and see each one appear as row-level change events.

    1. 1.Run: CREATE TABLE myschema.t(c1 INT, c2 STRING) TBLPROPERTIES(delta.enableChangeDataFeed=true);

      Why: Legacy CDF has to be switched on per table, here at creation time.

      You should see: DESCRIBE HISTORY shows version 0 as CREATE TABLE with delta.enableChangeDataFeed set to true in its properties.

    2. 2.Run the two INSERTs, then UPDATE myschema.t SET c2 = upper(c2) WHERE c1 < 3; then DELETE FROM myschema.t WHERE c1 = 3;

      Why: This gives you one commit of each kind: append, update and delete.

      You should see: DESCRIBE HISTORY shows versions 1–4: WRITE, WRITE, UPDATE, DELETE.

    3. 3.Run: SELECT * FROM table_changes('myschema.t', 2);

      Why: Reads every change from version 2 (inclusive) to the latest.

      You should see: One insert at version 2, two update_preimage/update_postimage pairs at version 3, and one delete at version 4.

    4. 4.Start spark.readStream.option("readChangeFeed", "true").table(...) on the table with trigger(availableNow=True), writing to a new target with its own checkpoint.

      Why: Shows what a stream emits the first time it runs without a starting version.

      You should see: The target receives the table's current rows as insert records.

    Stuck? Get a nudge

    Run one more UPDATE and rerun the availableNow stream. Only the new pre/post pair should arrive, because the checkpoint remembers where it stopped.

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “This is the most robust approach because your code explicitly handles every type of change event.”
      ↩︎ Why plain streaming breaks on updates and deletes
      “skipChangeCommits disregards file-changing operations entirely.”
      ↩︎ Why plain streaming breaks on updates and deletes
      “Unchanged rows are often emitted alongside new rows, so downstream consumers must be able to handle duplicates. Deletes are not propagated downstream.”
      ↩︎ Why plain streaming breaks on updates and deletes
      “To reflect changes in stream source tables, you must implement separate logic to propagate these changes.”
      ↩︎ Exam trap 2
      “Structured Streaming only accepts append inputs and throws an exception if any modifications occur on the source Delta Lake table.”
      ↩︎ Prediction
      “Databricks recommends streaming from the CDC feed of a Delta Lake table rather than directly from the table whenever possible.”
      ↩︎ Checkpoint
    2. 2.
      “For CDC processing with SCD type 1 or type 2 tables, see The AUTO CDC APIs”
      ↩︎ Why plain streaming breaks on updates and deletes
      “You must use Structured Streaming for Databricks to automatically track versions for your table's change data feed.”
      ↩︎ Configuring a readChangeFeed stream
      “Databricks supports rate limits (maxFilesPerTrigger, maxBytesPerTrigger) and excludeRegex when reading change data.”
      ↩︎ Configuring a readChangeFeed stream
      “Either the current batch includes the entire commit, or the current batch defers the commit to the next batch.”
      ↩︎ Configuring a readChangeFeed stream
      “It only records changes that occur after change data feed was enabled.”
      ↩︎ Retention, permanent history, and checkpoint recovery
      “The VACUUM command deletes change data files, and changes from the transaction log use checkpoint retention policy.”
      ↩︎ Retention, permanent history, and checkpoint recovery
      “use incremental logic to write records from the change data feed to a new table.”
      ↩︎ Retention, permanent history, and checkpoint recovery
      “Because managed tables clean up historic versions automatically, all specified starting versions are eventually deleted.”
      ↩︎ Retention, permanent history, and checkpoint recovery
      “When you define the write stream to the existing target table, you must specify a new checkpoint location”
      ↩︎ Retention, permanent history, and checkpoint recovery
      “A change data feed is not intended to serve as a permanent record of all changes to a table.”
      ↩︎ Exam trap 1
      “records all existing records in the table as INSERT operations when the stream first starts.”
      ↩︎ Exam trap 3
      “change data feed returns the latest snapshot of the table as INSERT records and then returns future changes as change data.”
      ↩︎ Prediction
      “specify a starting version to avoid processing the source table state as INSERT events.”
      ↩︎ Checkpoint
      “Records in the change data feed are transient and only accessible for a specified retention window.”
      ↩︎ Checkpoint

    Ready to test yourself?

    Practise Databricks Certified Data Engineer Professional in quiz mode.

    Spotted a mistake, or was something unclear? Tell us.