Delta Live Tables (DLT) – Slowly Changing Dimensions SCD Type 2

Delta Live Tables (DLT) is a declarative framework in Databricks for building reliable, automated ETL pipelines. It can automatically manage SCD Type 2 logic, tracking complete historical changes for dimension tables.

1. What SCD Type 2 Means

2. How DLT Implements SCD Type 2

DLT simplifies SCD2 by using the dlt.apply_changes function, which automatically:

3. Pipeline Structure

  1. A source table or stream (CDC feed or Silver table)
  2. A target dimension table in DLT
  3. A configuration using dlt.apply_changes with SCD Type 2 options

Example Source Table (Customer Changes)


@dlt.table
def customer_changes():
    return spark.readStream.table("silver.customer_changes")

Example Target Table (Dimension Seed)


@dlt.table
def dim_customer():
    return spark.read.table("dim.dim_customer_seed")

DLT SCD Type 2 Logic


dlt.apply_changes(
    target = "dim_customer",
    source = "customer_changes",
    keys = ["customer_id"],
    sequence_by = col("change_ts"),
    stored_as_scd_type = "2",
    track_history_columns = ["name", "address"]
)

4. Why Use DLT for SCD Type 2

5. Summary

  1. DLT reads change data from a source table or stream
  2. A dimension table is defined as a DLT-managed table
  3. dlt.apply_changes uses SCD2 logic to insert new versions and expire old ones
  4. The result is a fully historical, audit-friendly dimension table

6. How the history rows evolve

dim_customer rows for customer 7 after three change events t1 insert 7 Ana Oak St t1 null t2 address 7 Ana Elm St t2 null t2 old row 7 Ana Oak St t1 t2 t3 name 7 Ana M. Elm St t3 null columns: customer_id, name, address, __START_AT, __END_AT (null END = current row)

With stored_as_scd_type = "2", the target table gets two extra columns, __START_AT and __END_AT, populated from the sequence_by column. When a change arrives for an existing key and one of the tracked columns differs, the current row is closed by setting its __END_AT to the new event's sequence value, and a new row is inserted with __END_AT null. The current version of each key is therefore the row where __END_AT IS NULL, and the version valid at any instant T is the row where __START_AT <= T and __END_AT is null or greater than T.

Because events are ordered by sequence_by rather than by arrival time, late or out-of-order events are placed into the correct position in history. That is the main thing hand-written MERGE logic for SCD2 usually gets wrong.

7. Pipeline flow

CDC feed to SCD2 dimension CDC sourcestream of changes Expectationsquality checks apply_changeskeys + sequence_by dim_customerSCD2 history Current view__END_AT IS NULL

In a typical medallion pipeline the CDC feed (from Debezium, a database's change data feed, or a vendor such as Fivetran) lands in a bronze table, is cleaned into a silver streaming table with expectations that drop or quarantine malformed records, and is then applied to a gold dimension. The target of apply_changes must be declared with dlt.create_streaming_table; it is maintained entirely by the apply flow. Databricks has since renamed DLT to Lakeflow Declarative Pipelines and introduced create_auto_cdc_flow as the newer name for the same API; existing apply_changes code continues to work.

8. Worked example: complete pipeline

import dlt
from pyspark.sql.functions import col, expr

@dlt.view
@dlt.expect_or_drop("valid_key", "customer_id IS NOT NULL")
def customer_changes_clean():
    return spark.readStream.table("silver.customer_changes")

dlt.create_streaming_table("dim_customer")

dlt.apply_changes(
    target="dim_customer",
    source="customer_changes_clean",
    keys=["customer_id"],
    sequence_by=col("change_ts"),
    apply_as_deletes=expr("op = 'DELETE'"),
    except_column_list=["op"],
    stored_as_scd_type=2,
    track_history_column_list=["name", "address"],
)

@dlt.table(comment="Current row per customer")
def dim_customer_current():
    return dlt.read("dim_customer").where("__END_AT IS NULL")

track_history_column_list (or track_history_except_column_list) restricts which columns create new versions; changes to other columns update the current row in place. apply_as_deletes closes the current row when a delete event arrives.

9. SCD Type 1 vs Type 2 vs snapshots

ApproachHistoryRows per keyUse when
SCD Type 1None, overwrite1Corrections, attributes nobody reports on historically
SCD Type 2Full, row per versionManyPoint-in-time reporting: revenue by the customer's region at the time of sale
Delta time travelTable-level versions, limited by retention1Auditing and recovery, not modelled history
Periodic snapshotsFull copy per period1 per periodSimple sources with no change feed

10. Pitfalls