8.5 Delta Change Data Feed (CDF) & MERGE INTO Upsert Patterns
Key Takeaways
- Delta Change Data Feed (CDF) captures row-level changes (inserts, updates, deletes) committed to a Delta table, allowing downstream consumers to process incremental changes efficiently.
- Enabling CDF on a table (`delta.enableChangeDataFeed = true`) automatically generates CDC metadata columns: `_change_type`, `_commit_version`, and `_commit_timestamp`.
- `_change_type` outputs four distinct row actions: `insert`, `delete`, `update_preimage` (row state before update), and `update_postimage` (row state after update).
- `MERGE INTO` provides atomic upsert operations, matching source and target records on a join condition to execute conditional `UPDATE`, `DELETE`, and `INSERT` actions in a single transaction.
- Slowly Changing Dimensions (SCD) are implemented using MERGE: SCD Type 1 performs in-place overwrites, while SCD Type 2 maintains historical record versions using effective date ranges and active flags.
8.5 Delta Change Data Feed (CDF) & MERGE INTO Upsert Patterns
In modern enterprise data lakehouses, downstream Silver and Gold tables frequently depend on changes occurring in upstream Bronze tables. Traditionally, propagating row-level inserts, updates, and deletes required either computationally expensive full-table diffs or custom timestamp watermarking that failed to capture deleted rows.
Delta Lake solves this with Change Data Feed (CDF)—a native mechanism that exposes row-level change events from the transaction log—and MERGE INTO, the ANSI SQL standard for atomic upsert and Slowly Changing Dimension (SCD) data pipeline processing.
1. Delta Change Data Feed (CDF) Architecture
Change Data Feed records row-level modifications made to a Delta table. When enabled, every INSERT, UPDATE, DELETE, and MERGE operation generates structured CDC event records alongside the standard Parquet payload files.
+-------------------------------------------------------------------------+
| CHANGE DATA FEED (CDF) WORKFLOW |
+-------------------------------------------------------------------------+
| |
| [ Upstream App / Source ] ---> [ Bronze Delta Table (CDF Enabled) ] |
| | |
| +---------------+---------------+ |
| | CDF Micro-Batch Streaming | |
| v v |
| [ Silver Aggregations ] [ Audit Compliance ] |
+-------------------------------------------------------------------------+
How to Enable Change Data Feed
1. On Table Creation:
CREATE TABLE customers_bronze (
customer_id BIGINT,
name STRING,
email STRING,
status STRING,
updated_at TIMESTAMP
)
USING DELTA
TBLPROPERTIES (delta.enableChangeDataFeed = true);
2. On Existing Tables via ALTER TABLE:
ALTER TABLE customers_bronze
SET TBLPROPERTIES (delta.enableChangeDataFeed = true);
3. Workspace-Wide or Session-Wide Default:
SET spark.databricks.delta.properties.defaults.enableChangeDataFeed = true;
2. CDF Metadata Columns & Change Types
When querying Change Data Feed, Delta Lake returns all underlying table columns plus three critical CDC metadata columns:
| Metadata Column | Data Type | Description |
|---|---|---|
_change_type | StringType | Identifies the nature of the row modification: insert, delete, update_preimage, or update_postimage. |
_commit_version | LongType | The Delta table transaction log commit version that generated this change. |
_commit_timestamp | TimestampType | The UTC timestamp when the transaction was committed to the log. |
The Four _change_type Values
+-------------------------------------------------------------------------+
| _change_type VALUES |
+-------------------------------------------------------------------------+
| 1. insert | Row added via INSERT, COPY INTO, or MERGE |
| 2. delete | Row removed via DELETE or MERGE |
| 3. update_preimage | Row values BEFORE an UPDATE was applied |
| 4. update_postimage | Row values AFTER an UPDATE was applied |
+-------------------------------------------------------------------------+
Exam Tip: For every updated row, CDF produces two records: an
update_preimagerepresenting the state before the update, and anupdate_postimagerepresenting the state after the update. Both records share the exact same_commit_versionand_commit_timestamp.
3. Querying Change Data Feed (Batch & Streaming)
1. Batch SQL Query using table_changes()
-- Query all changes between commit version 5 and version 12
SELECT * FROM table_changes('customers_bronze', 5, 12);
-- Query all changes since a specific timestamp
SELECT * FROM table_changes('customers_bronze', '2026-08-20 00:00:00');
2. PySpark Structured Streaming with CDF
Downstream consumers can stream only the incremental changes directly from the CDF stream:
# Read incremental changes from upstream table using Structured Streaming
df_cdf_stream = spark.readStream \
.format("delta") \
.option("readChangeFeed", "true") \
.option("startingVersion", 5) \
.table("customers_bronze")
# Filter only updated and inserted records for downstream Silver upsert
df_clean_changes = df_cdf_stream.filter(
"_change_type IN ('insert', 'update_postimage')"
)
4. Atomic Upserts with MERGE INTO
MERGE INTO is an ANSI SQL statement that enables conditional multi-operation updates, inserts, and deletes against a target Delta table within a single atomic transaction.
MERGE INTO customers_silver AS target
USING incoming_updates AS source
ON target.customer_id = source.customer_id
WHEN MATCHED AND source.status = 'DELETED' THEN
DELETE
WHEN MATCHED AND target.updated_at < source.updated_at THEN
UPDATE SET
target.name = source.name,
target.email = source.email,
target.status = source.status,
target.updated_at = source.updated_at
WHEN NOT MATCHED THEN
INSERT (customer_id, name, email, status, updated_at)
VALUES (source.customer_id, source.name, source.email, source.status, source.updated_at);
Rules and Constraints of MERGE INTO
- Deterministic Matching: The
ONjoin condition must match each target row to at most one source row. If multiple source rows match the same target row, the query fails with a runtime error:
org.apache.spark.sql.delta.DeltaUnsupportedOperationException:
Cannot perform Merge as multiple source rows matched the same target row.
- Clause Order:
WHEN MATCHEDclauses are evaluated in the sequential order written; only the first matching condition executes.
5. Implementing Slowly Changing Dimensions (SCD Type 1 & 2)
Data warehousing architectures classify dimensional updates into distinct patterns:
1. SCD Type 1 (In-Place Overwrite)
SCD Type 1 overwrites existing dimension attributes without retaining historical version records. It is implemented with standard WHEN MATCHED THEN UPDATE SET *.
2. SCD Type 2 (Historical Tracking)
SCD Type 2 preserves complete history by expiring old records (setting is_current = false and end_date = current_timestamp()) and inserting new records (with is_current = true, start_date = current_timestamp(), end_date = NULL).
-- Step 1: Stage merged dataset containing updates to close and new rows to insert
MERGE INTO customer_dimension AS target
USING (
-- Source records to insert (both brand new customers and new versions of existing customers)
SELECT source.customer_id AS merge_key, source.*
FROM source_updates source
UNION ALL
-- Flag existing active target records that have changed so they can be closed (expired)
SELECT NULL AS merge_key, source.*
FROM source_updates source
JOIN customer_dimension target
ON target.customer_id = source.customer_id
AND target.is_current = true
AND (target.email <> source.email OR target.status <> source.status)
) AS staged_changes
ON target.customer_id = staged_changes.merge_key
AND target.is_current = true
-- Close active record (expire old version)
WHEN MATCHED AND (target.email <> staged_changes.email OR target.status <> staged_changes.status) THEN
UPDATE SET
target.is_current = false,
target.end_date = current_timestamp()
-- Insert new active record version (or brand new customer)
WHEN NOT MATCHED THEN
INSERT (customer_id, name, email, status, start_date, end_date, is_current)
VALUES (staged_changes.customer_id, staged_changes.name, staged_changes.email,
staged_changes.status, current_timestamp(), NULL, true);
6. MERGE Performance Optimization Best Practices
- Target Partition / Clustering Pruning: Always include partition keys or Liquid Clustering columns in the
ONjoin predicate (e.g.,ON target.date = source.date AND target.id = source.id). This prevents Delta Lake from scanning entire tables. - Deduplicate Source Data: Ensure the source DataFrame has no duplicate keys before running
MERGEto avoid multiple-match exceptions. - Compact Source Data: If the source DataFrame consists of thousands of micro-batch files, coalesce or compact source data before executing the merge.
- Enable Low Shuffle Merge: In Databricks Runtime, low shuffle merge optimizes join execution when only a small percentage of target files are modified (
SET spark.databricks.delta.merge.lowShuffle.enabled = true).
When querying a Delta table with Change Data Feed (CDF) enabled, what metadata records are produced when an existing row is modified by an UPDATE statement?
A data engineer runs a MERGE INTO SQL statement to upsert daily retail sales into a target Delta table. The query fails at runtime with the error Cannot perform Merge as multiple source rows matched the same target row. What is the cause of this error?
In a lakehouse dimension table tracking customer addresses, business requirements dictate that whenever a customer changes their address, the previous address must be preserved with an expiration timestamp and a new active record must be created. Which dimensional design pattern and implementation fulfills this requirement?