<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Building a Production‑Style SCD Type 2 Dimension on Delta Lake — Using Databricks Community Edition in Community Articles</title>
    <link>https://community.databricks.com/t5/community-articles/building-a-production-style-scd-type-2-dimension-on-delta-lake/m-p/149990#M1057</link>
    <description>&lt;DIV&gt;&lt;P&gt;If you’ve ever needed to maintain &lt;STRONG&gt;historical truth in a data warehouse, you’ve likely bumped into &lt;STRONG&gt;Slowly Changing Dimensions (SCD)—specifically &lt;STRONG&gt;Type 2. In SCD2, we keep every version of a record as it changes over time, so analysis can answer questions like &lt;EM&gt;“What was the customer’s region when they placed that order in March?”.&lt;/EM&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;In this post, I’ll show you how to implement a &lt;STRONG&gt;robust, production‑style SCD Type 2 pipeline on Delta Lake using &lt;STRONG&gt;Databricks Community Edition (CE). No enterprise bells and whistles—just lean, elegant techniques you can run today.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV&gt;&lt;P&gt;&lt;STRONG&gt;What you’ll learn&lt;/STRONG&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;A clean SCD2 schema pattern for Delta tables&lt;/LI&gt;&lt;LI&gt;How to compute change detection at scale (using hashes)&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;two‑phase upsert strategy that’s easy to reason about&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;How to add &lt;STRONG&gt;time travel, &lt;STRONG&gt;auditing, and &lt;STRONG&gt;vacuum hygiene&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Optional performance tuning tips that work even on CE&lt;DIV&gt;&lt;H2&gt;Prerequisites&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;A running &lt;STRONG&gt;cluster in Databricks Community Edition&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;Python notebook (SQL will be used where it makes sense)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Basic familiarity with Spark DataFrames and Delta tables&lt;DIV&gt;&lt;H2&gt;The SCD2 Table Shape (Delta)&lt;/H2&gt;&lt;P&gt;We’ll maintain a dimension table with the following columns:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;business_key: natural key (e.g., customer_id)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;attributes: e.g., name, region, segment&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;valid_from, &lt;STRONG&gt;valid_to: versioning window&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;is_current: fast filter for the latest version&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;record_hash: hash of attributes for quick change detection&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;ingest_ts and &lt;STRONG&gt;batch_id: for auditing lineage&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;BLOCKQUOTE&gt;&lt;P&gt;&lt;STRONG&gt;Tip: This pattern keeps history queries simple and makes current-state lookups blazing fast.&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV&gt;&lt;H2&gt;Step 1: Set Up a Workspace and Some Sample Data&lt;/H2&gt;&lt;P&gt;We’ll create a small, realistic dataset with a few evolving records.&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import types as T&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Workspace config&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;catalog_db = "scd2_demo_db"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql(f"CREATE DATABASE IF NOT EXISTS {catalog_db}")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql(f"USE {catalog_db}")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Helper: stable hash for change detection (pick the cols you care about)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;def hash_cols(*cols):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return F.sha2(F.concat_ws("||", *[F.coalesce(c.cast("string"), F.lit("")) for c in cols]), 256)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Initial batch (b1)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;data_b1 = [&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (101, "Asha",&amp;nbsp;&amp;nbsp; "North", "SMB"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (102, "Ravi",&amp;nbsp;&amp;nbsp; "West",&amp;nbsp; "Enterprise"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (103, "Meera",&amp;nbsp; "South", "MidMarket"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;]&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;schema = T.StructType([&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("customer_id", T.IntegerType(), False),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("name",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("region",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("segment",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;])&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_df = spark.createDataFrame(data_b1, schema) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("record_hash", hash_cols(F.col("name"), F.col("region"), F.col("segment"))) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit("b1")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp())&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_df.display()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Step 2: Create the SCD2 Delta Table&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;CREATE TABLE IF NOT EXISTS scd2_customer_dim (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; customer_id INT NOT NULL,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; name&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; region&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; segment&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; valid_from&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; valid_to&amp;nbsp;&amp;nbsp;&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; is_current&amp;nbsp; BOOLEAN,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; record_hash STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; ingest_ts&amp;nbsp;&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; batch_id&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;USING DELTA;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Optional: add simple expectation-like constraints you’ll police in your pipeline&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- (In CE, we do this via queries/process rather than Unity Catalog-level constraints.)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Load the first batch as the initial current state:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_for_insert = b1_df.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment","record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;.withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;.withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_for_insert.write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Step 3: The Two‑Phase SCD2 Algorithm&lt;DIV&gt;&lt;DIV&gt;&lt;P&gt;Why two phases? Because a single MERGE can get tricky when you need to both &lt;STRONG&gt;expire the old version and &lt;STRONG&gt;insert a new one for the same key in the same run. The two‑phase approach is explicit, auditable, and easy to test.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Phase A — Detect and expire changed records&lt;BR /&gt;&lt;STRONG&gt;Phase B — Insert new versions for changed or brand‑new records&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;DIV&gt;&lt;H2&gt;Step 4: Simulate a New Ingest (Changes + New Rows)&lt;/H2&gt;&lt;P&gt;Let’s say:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Ravi moves from &lt;STRONG&gt;West → North (change)&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Meera is unchanged&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;New customer &lt;STRONG&gt;Kiran arrives&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;data_b2 = [&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (101, "Asha",&amp;nbsp; "North", "SMB"),&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # unchanged&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (102, "Ravi",&amp;nbsp; "North", "Enterprise"),&amp;nbsp; # changed region&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (103, "Meera", "South", "MidMarket"),&amp;nbsp;&amp;nbsp; # unchanged&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (104, "Kiran", "East",&amp;nbsp; "SMB"),&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # brand new&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;]&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b2_df = spark.createDataFrame(data_b2, schema) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("record_hash", hash_cols(F.col("name"), F.col("region"), F.col("segment"))) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit("b2")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp())&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Phase A — Detect Changes and Expire Old Versions&lt;/H2&gt;&lt;OL&gt;&lt;LI&gt;Identify the &lt;STRONG&gt;current target rows (fast filter on is_current = true).&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Join with the incoming batch on the business key.&lt;/LI&gt;&lt;LI&gt;Mark those where the hash differs as &lt;STRONG&gt;changed.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Expire those in the target (set valid_to and is_current=false).&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;target_cur = spark.table("scd2_customer_dim").where("is_current = true").select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","record_hash"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).withColumnRenamed("record_hash","tgt_hash")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;incoming = b2_df.select("customer_id","record_hash")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_keys = incoming.join(target_cur, "customer_id", "inner") \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .where(F.col("record_hash") != F.col("tgt_hash")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_keys.createOrReplaceTempView("changed_keys_view")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Expire the previous versions for changed keys&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; UPDATE scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; SET valid_to = current_timestamp(), is_current = false&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND customer_id IN (SELECT customer_id FROM changed_keys_view)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;""")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Phase B — Insert New Versions (Changed + Brand‑New)&lt;/H2&gt;&lt;P&gt;We’ll prepare two sets:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;changed_now: keys we just expired&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;brand_new: keys never seen before&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Changed records (for new version rows)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_now = b2_df.join(changed_keys, "customer_id", "inner")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Brand new keys: anti-join against ANY historical target row&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;all_target_keys = spark.table("scd2_customer_dim").select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;brand_new = b2_df.join(all_target_keys, "customer_id", "left_anti")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;to_insert = changed_now.unionByName(brand_new).withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;to_insert.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "valid_from","valid_to","is_current",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Validate the result:&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SELECT customer_id, name, region, segment, is_current, valid_from, valid_to, batch_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;FROM scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;ORDER BY customer_id, valid_from;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;You should now see &lt;STRONG&gt;&lt;STRONG&gt;Ravi’s prior record closed (is_current=false, valid_to filled) and a &lt;STRONG&gt;new current row with region='North'. &lt;STRONG&gt;Kiran appears as a first‑time insert.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Time Travel (Debugging &amp;amp; Audits)&lt;/H2&gt;&lt;P&gt;Delta Lake’s time travel makes SCD2 &lt;EM&gt;pleasant to operate. Check historical snapshots:&lt;/EM&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;DESCRIBE HISTORY scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Pick a version from the history output:&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SELECT * FROM scd2_customer_dim VERSION AS OF 0;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Hygiene: Vacuum &amp;amp; Retention&lt;/H2&gt;&lt;P&gt;CE is perfect for practicing &lt;STRONG&gt;data lifecycle management:&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- See current retention&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SHOW TBLPROPERTIES scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Clean up old files (default retention 7 days; don’t go below unless you know what you’re doing)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;VACUUM scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;``&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;span class="lia-unicode-emoji" title=":warning:"&gt;⚠️&lt;/span&gt; If you tweak retention below 7 days for learning, remember you’ll lose the ability to time travel that far back.&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Optional: Performance Tips That Work in CE&lt;/H2&gt;&lt;P&gt;Even small clusters benefit from good habits:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;&lt;STRONG&gt;Selective columns in joins and writes — trim the DataFrame early.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Predicate pushdown — filter by is_current before joining.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Skew awareness — if one key is super hot, try salting (add a random bucket on the source, then aggregate).&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;Optimize small dimensions — broadcast join them when referencing from facts:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql.functions import broadcast&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;fact_join = facts_df.join(broadcast(spark.table("scd2_customer_dim").where("is_current = true")),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id", "left")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Z-Ordering / Optimize — if your CE runtime exposes it in your account, you can try:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;OPTIMIZE scd2_customer_dim ZORDER BY (customer_id);&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;DIV&gt;&lt;H2&gt;Turning This Into a Stream (Bonus)&lt;/H2&gt;&lt;P&gt;When you’re ready, you can convert the batch logic into micro‑batch streaming using foreachBatch and call the &lt;STRONG&gt;same two‑phase functions:&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;def scd2_upsert(batch_df, batch_id):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # compute hashes, then run Phase A + Phase B against scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; incoming = batch_df.withColumn("record_hash", hash_cols("name","region","segment")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit(str(batch_id)))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; incoming.createOrReplaceTempView("incoming_view")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Phase A (expire changed)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WITH tgt_cur AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash AS tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM scd2_customer_dim WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; inc AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash FROM incoming_view&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT inc.customer_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM inc JOIN tgt_cur USING (customer_id)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE inc.record_hash &amp;lt;&amp;gt; tgt_cur.tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; UPDATE scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET valid_to = current_timestamp(), is_current = false&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND customer_id IN (SELECT customer_id FROM changed)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; """)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Phase B (insert changed + brand-new)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed_keys_df = spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WITH tgt_cur AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash AS tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM scd2_customer_dim WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; inc AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash FROM incoming_view&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT inc.customer_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM inc JOIN tgt_cur USING (customer_id)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE inc.record_hash &amp;lt;&amp;gt; tgt_cur.tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; """).distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed_now = incoming.join(changed_keys_df, "customer_id", "inner")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; all_target_keys = spark.table("scd2_customer_dim").select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; brand_new = incoming.join(all_target_keys, "customer_id", "left_anti")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; to_insert = changed_now.unionByName(brand_new) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; to_insert.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "valid_from","valid_to","is_current",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ).write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Example streaming source: drop CSVs into /FileStore/landing/customers&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;landing_path = "/FileStore/landing/customers"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;schema_stream = schema&amp;nbsp; # reuse&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.readStream.format("csv").schema(schema_stream).option("header", True).load(landing_path) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .writeStream.outputMode("append").trigger(processingTime="10 seconds") \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .foreachBatch(scd2_upsert).start()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Note: CE doesn’t provide Auto Loader, but file‑based streams work well for demos. Just upload small CSVs to the landing path between triggers.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Why This Approach Works in the Real World&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Deterministic logic: expire → insert is explicit and testable&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Auditable: every row has valid_from/valid_to, batch_id, ingest_ts&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Fast queries: is_current = true makes most BI joins trivial&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;Time travel: snapshot consistency for investigations and “as‑of” analytics&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;H2&gt;How to Personalize This Post (to keep it uniquely yours)&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;Add a &lt;STRONG&gt;real scenario (e.g., “We changed territory mappings in Q4 and needed backtesting”)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Drop in &lt;STRONG&gt;screenshots of your notebook outputs and DESCRIBE HISTORY&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Mention a &lt;STRONG&gt;gotcha you hit (e.g., forgetting to expire before inserting and getting duplicates)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Close with &lt;STRONG&gt;&lt;STRONG&gt;metrics: how many rows processed, runtime on CE, etc.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;H2&gt;Recap&lt;/H2&gt;&lt;P&gt;You just built a &lt;STRONG&gt;clean, production‑style SCD Type 2 pipeline on &lt;STRONG&gt;Delta Lake using &lt;STRONG&gt;Databricks Community Edition:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;Two‑phase upsert (expire then insert)&lt;/LI&gt;&lt;LI&gt;Hash‑based change detection&lt;/LI&gt;&lt;LI&gt;Delta time travel + vacuum hygiene&lt;/LI&gt;&lt;LI&gt;Optional tuning that works even on small clusters&lt;P&gt;If you’d like, I can turn this into:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;A &lt;STRONG&gt;LinkedIn‑ready shorter version (700–900 words)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;GitHub gist with the full notebook code&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;diagram of the SCD2 flow (PNG/SVG)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A follow‑up on &lt;STRONG&gt;&lt;STRONG&gt;incremental facts joining to current dimensions&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline"&gt;&lt;img /&gt;&lt;/span&gt;&lt;/P&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/OL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/OL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/BLOCKQUOTE&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;</description>
    <pubDate>Fri, 06 Mar 2026 12:19:52 GMT</pubDate>
    <dc:creator>Prosenjeet33</dc:creator>
    <dc:date>2026-03-06T12:19:52Z</dc:date>
    <item>
      <title>Building a Production‑Style SCD Type 2 Dimension on Delta Lake — Using Databricks Community Edition</title>
      <link>https://community.databricks.com/t5/community-articles/building-a-production-style-scd-type-2-dimension-on-delta-lake/m-p/149990#M1057</link>
      <description>&lt;DIV&gt;&lt;P&gt;If you’ve ever needed to maintain &lt;STRONG&gt;historical truth in a data warehouse, you’ve likely bumped into &lt;STRONG&gt;Slowly Changing Dimensions (SCD)—specifically &lt;STRONG&gt;Type 2. In SCD2, we keep every version of a record as it changes over time, so analysis can answer questions like &lt;EM&gt;“What was the customer’s region when they placed that order in March?”.&lt;/EM&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;In this post, I’ll show you how to implement a &lt;STRONG&gt;robust, production‑style SCD Type 2 pipeline on Delta Lake using &lt;STRONG&gt;Databricks Community Edition (CE). No enterprise bells and whistles—just lean, elegant techniques you can run today.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV&gt;&lt;P&gt;&lt;STRONG&gt;What you’ll learn&lt;/STRONG&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;A clean SCD2 schema pattern for Delta tables&lt;/LI&gt;&lt;LI&gt;How to compute change detection at scale (using hashes)&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;two‑phase upsert strategy that’s easy to reason about&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;How to add &lt;STRONG&gt;time travel, &lt;STRONG&gt;auditing, and &lt;STRONG&gt;vacuum hygiene&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Optional performance tuning tips that work even on CE&lt;DIV&gt;&lt;H2&gt;Prerequisites&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;A running &lt;STRONG&gt;cluster in Databricks Community Edition&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;Python notebook (SQL will be used where it makes sense)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Basic familiarity with Spark DataFrames and Delta tables&lt;DIV&gt;&lt;H2&gt;The SCD2 Table Shape (Delta)&lt;/H2&gt;&lt;P&gt;We’ll maintain a dimension table with the following columns:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;business_key: natural key (e.g., customer_id)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;attributes: e.g., name, region, segment&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;valid_from, &lt;STRONG&gt;valid_to: versioning window&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;is_current: fast filter for the latest version&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;record_hash: hash of attributes for quick change detection&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;ingest_ts and &lt;STRONG&gt;batch_id: for auditing lineage&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;BLOCKQUOTE&gt;&lt;P&gt;&lt;STRONG&gt;Tip: This pattern keeps history queries simple and makes current-state lookups blazing fast.&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV&gt;&lt;H2&gt;Step 1: Set Up a Workspace and Some Sample Data&lt;/H2&gt;&lt;P&gt;We’ll create a small, realistic dataset with a few evolving records.&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import types as T&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Workspace config&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;catalog_db = "scd2_demo_db"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql(f"CREATE DATABASE IF NOT EXISTS {catalog_db}")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql(f"USE {catalog_db}")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Helper: stable hash for change detection (pick the cols you care about)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;def hash_cols(*cols):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return F.sha2(F.concat_ws("||", *[F.coalesce(c.cast("string"), F.lit("")) for c in cols]), 256)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Initial batch (b1)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;data_b1 = [&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (101, "Asha",&amp;nbsp;&amp;nbsp; "North", "SMB"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (102, "Ravi",&amp;nbsp;&amp;nbsp; "West",&amp;nbsp; "Enterprise"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (103, "Meera",&amp;nbsp; "South", "MidMarket"),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;]&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;schema = T.StructType([&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("customer_id", T.IntegerType(), False),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("name",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("region",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StructField("segment",&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; T.StringType(),&amp;nbsp; True),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;])&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_df = spark.createDataFrame(data_b1, schema) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("record_hash", hash_cols(F.col("name"), F.col("region"), F.col("segment"))) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit("b1")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp())&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_df.display()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Step 2: Create the SCD2 Delta Table&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;CREATE TABLE IF NOT EXISTS scd2_customer_dim (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; customer_id INT NOT NULL,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; name&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; region&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; segment&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; valid_from&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; valid_to&amp;nbsp;&amp;nbsp;&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; is_current&amp;nbsp; BOOLEAN,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; record_hash STRING,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; ingest_ts&amp;nbsp;&amp;nbsp; TIMESTAMP,&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; batch_id&amp;nbsp;&amp;nbsp;&amp;nbsp; STRING&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;USING DELTA;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Optional: add simple expectation-like constraints you’ll police in your pipeline&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- (In CE, we do this via queries/process rather than Unity Catalog-level constraints.)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Load the first batch as the initial current state:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_for_insert = b1_df.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment","record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;.withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;.withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b1_for_insert.write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Step 3: The Two‑Phase SCD2 Algorithm&lt;DIV&gt;&lt;DIV&gt;&lt;P&gt;Why two phases? Because a single MERGE can get tricky when you need to both &lt;STRONG&gt;expire the old version and &lt;STRONG&gt;insert a new one for the same key in the same run. The two‑phase approach is explicit, auditable, and easy to test.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Phase A — Detect and expire changed records&lt;BR /&gt;&lt;STRONG&gt;Phase B — Insert new versions for changed or brand‑new records&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;DIV&gt;&lt;H2&gt;Step 4: Simulate a New Ingest (Changes + New Rows)&lt;/H2&gt;&lt;P&gt;Let’s say:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Ravi moves from &lt;STRONG&gt;West → North (change)&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Meera is unchanged&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;New customer &lt;STRONG&gt;Kiran arrives&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;data_b2 = [&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (101, "Asha",&amp;nbsp; "North", "SMB"),&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # unchanged&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (102, "Ravi",&amp;nbsp; "North", "Enterprise"),&amp;nbsp; # changed region&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (103, "Meera", "South", "MidMarket"),&amp;nbsp;&amp;nbsp; # unchanged&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; (104, "Kiran", "East",&amp;nbsp; "SMB"),&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # brand new&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;]&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;b2_df = spark.createDataFrame(data_b2, schema) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("record_hash", hash_cols(F.col("name"), F.col("region"), F.col("segment"))) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit("b2")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp())&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Phase A — Detect Changes and Expire Old Versions&lt;/H2&gt;&lt;OL&gt;&lt;LI&gt;Identify the &lt;STRONG&gt;current target rows (fast filter on is_current = true).&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Join with the incoming batch on the business key.&lt;/LI&gt;&lt;LI&gt;Mark those where the hash differs as &lt;STRONG&gt;changed.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Expire those in the target (set valid_to and is_current=false).&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;target_cur = spark.table("scd2_customer_dim").where("is_current = true").select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","record_hash"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).withColumnRenamed("record_hash","tgt_hash")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;incoming = b2_df.select("customer_id","record_hash")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_keys = incoming.join(target_cur, "customer_id", "inner") \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .where(F.col("record_hash") != F.col("tgt_hash")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_keys.createOrReplaceTempView("changed_keys_view")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Expire the previous versions for changed keys&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; UPDATE scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; SET valid_to = current_timestamp(), is_current = false&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp; WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND customer_id IN (SELECT customer_id FROM changed_keys_view)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;""")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Phase B — Insert New Versions (Changed + Brand‑New)&lt;/H2&gt;&lt;P&gt;We’ll prepare two sets:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;changed_now: keys we just expired&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;brand_new: keys never seen before&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Changed records (for new version rows)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;changed_now = b2_df.join(changed_keys, "customer_id", "inner")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Brand new keys: anti-join against ANY historical target row&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;all_target_keys = spark.table("scd2_customer_dim").select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;brand_new = b2_df.join(all_target_keys, "customer_id", "left_anti")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;to_insert = changed_now.unionByName(brand_new).withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;to_insert.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "valid_from","valid_to","is_current",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; "record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;).write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;Validate the result:&lt;DIV&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SELECT customer_id, name, region, segment, is_current, valid_from, valid_to, batch_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;FROM scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;ORDER BY customer_id, valid_from;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;You should now see &lt;STRONG&gt;&lt;STRONG&gt;Ravi’s prior record closed (is_current=false, valid_to filled) and a &lt;STRONG&gt;new current row with region='North'. &lt;STRONG&gt;Kiran appears as a first‑time insert.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Time Travel (Debugging &amp;amp; Audits)&lt;/H2&gt;&lt;P&gt;Delta Lake’s time travel makes SCD2 &lt;EM&gt;pleasant to operate. Check historical snapshots:&lt;/EM&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;DESCRIBE HISTORY scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Pick a version from the history output:&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SELECT * FROM scd2_customer_dim VERSION AS OF 0;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;H2&gt;Hygiene: Vacuum &amp;amp; Retention&lt;/H2&gt;&lt;P&gt;CE is perfect for practicing &lt;STRONG&gt;data lifecycle management:&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- See current retention&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;SHOW TBLPROPERTIES scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;-- Clean up old files (default retention 7 days; don’t go below unless you know what you’re doing)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;VACUUM scd2_customer_dim;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;``&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;span class="lia-unicode-emoji" title=":warning:"&gt;⚠️&lt;/span&gt; If you tweak retention below 7 days for learning, remember you’ll lose the ability to time travel that far back.&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Optional: Performance Tips That Work in CE&lt;/H2&gt;&lt;P&gt;Even small clusters benefit from good habits:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;&lt;STRONG&gt;Selective columns in joins and writes — trim the DataFrame early.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Predicate pushdown — filter by is_current before joining.&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Skew awareness — if one key is super hot, try salting (add a random bucket on the source, then aggregate).&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;Optimize small dimensions — broadcast join them when referencing from facts:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;from pyspark.sql.functions import broadcast&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;fact_join = facts_df.join(broadcast(spark.table("scd2_customer_dim").where("is_current = true")),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id", "left")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Z-Ordering / Optimize — if your CE runtime exposes it in your account, you can try:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;OPTIMIZE scd2_customer_dim ZORDER BY (customer_id);&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;DIV&gt;&lt;H2&gt;Turning This Into a Stream (Bonus)&lt;/H2&gt;&lt;P&gt;When you’re ready, you can convert the batch logic into micro‑batch streaming using foreachBatch and call the &lt;STRONG&gt;same two‑phase functions:&lt;/STRONG&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;def scd2_upsert(batch_df, batch_id):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # compute hashes, then run Phase A + Phase B against scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; from pyspark.sql import functions as F&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; incoming = batch_df.withColumn("record_hash", hash_cols("name","region","segment")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("ingest_ts", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("batch_id", F.lit(str(batch_id)))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; incoming.createOrReplaceTempView("incoming_view")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Phase A (expire changed)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WITH tgt_cur AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash AS tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM scd2_customer_dim WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; inc AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash FROM incoming_view&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT inc.customer_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM inc JOIN tgt_cur USING (customer_id)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE inc.record_hash &amp;lt;&amp;gt; tgt_cur.tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; UPDATE scd2_customer_dim&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET valid_to = current_timestamp(), is_current = false&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND customer_id IN (SELECT customer_id FROM changed)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; """)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Phase B (insert changed + brand-new)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed_keys_df = spark.sql("""&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WITH tgt_cur AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash AS tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM scd2_customer_dim WHERE is_current = true&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ),&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; inc AS (&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT customer_id, record_hash FROM incoming_view&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT inc.customer_id&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM inc JOIN tgt_cur USING (customer_id)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHERE inc.record_hash &amp;lt;&amp;gt; tgt_cur.tgt_hash&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; """).distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; changed_now = incoming.join(changed_keys_df, "customer_id", "inner")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; all_target_keys = spark.table("scd2_customer_dim").select("customer_id").distinct()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; brand_new = incoming.join(all_target_keys, "customer_id", "left_anti")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; to_insert = changed_now.unionByName(brand_new) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_from", F.current_timestamp()) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("valid_to",&amp;nbsp;&amp;nbsp; F.lit(None).cast("timestamp")) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .withColumn("is_current", F.lit(True))&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; to_insert.select(&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "customer_id","name","region","segment",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "valid_from","valid_to","is_current",&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; "record_hash","ingest_ts","batch_id"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ).write.format("delta").mode("append").saveAsTable("scd2_customer_dim")&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;# Example streaming source: drop CSVs into /FileStore/landing/customers&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;landing_path = "/FileStore/landing/customers"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;P&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;schema_stream = schema&amp;nbsp; # reuse&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;spark.readStream.format("csv").schema(schema_stream).option("header", True).load(landing_path) \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .writeStream.outputMode("append").trigger(processingTime="10 seconds") \&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .foreachBatch(scd2_upsert).start()&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;DIV&gt;&lt;STRONG&gt;&lt;STRONG&gt;Note: CE doesn’t provide Auto Loader, but file‑based streams work well for demos. Just upload small CSVs to the landing path between triggers.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;H2&gt;Why This Approach Works in the Real World&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Deterministic logic: expire → insert is explicit and testable&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Auditable: every row has valid_from/valid_to, batch_id, ingest_ts&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Fast queries: is_current = true makes most BI joins trivial&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;&lt;STRONG&gt;Time travel: snapshot consistency for investigations and “as‑of” analytics&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;H2&gt;How to Personalize This Post (to keep it uniquely yours)&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;Add a &lt;STRONG&gt;real scenario (e.g., “We changed territory mappings in Q4 and needed backtesting”)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Drop in &lt;STRONG&gt;screenshots of your notebook outputs and DESCRIBE HISTORY&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Mention a &lt;STRONG&gt;gotcha you hit (e.g., forgetting to expire before inserting and getting duplicates)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;Close with &lt;STRONG&gt;&lt;STRONG&gt;metrics: how many rows processed, runtime on CE, etc.&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV&gt;&lt;H2&gt;Recap&lt;/H2&gt;&lt;P&gt;You just built a &lt;STRONG&gt;clean, production‑style SCD Type 2 pipeline on &lt;STRONG&gt;Delta Lake using &lt;STRONG&gt;Databricks Community Edition:&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;Two‑phase upsert (expire then insert)&lt;/LI&gt;&lt;LI&gt;Hash‑based change detection&lt;/LI&gt;&lt;LI&gt;Delta time travel + vacuum hygiene&lt;/LI&gt;&lt;LI&gt;Optional tuning that works even on small clusters&lt;P&gt;If you’d like, I can turn this into:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;A &lt;STRONG&gt;LinkedIn‑ready shorter version (700–900 words)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;GitHub gist with the full notebook code&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A &lt;STRONG&gt;diagram of the SCD2 flow (PNG/SVG)&lt;/STRONG&gt;&lt;/LI&gt;&lt;LI&gt;A follow‑up on &lt;STRONG&gt;&lt;STRONG&gt;incremental facts joining to current dimensions&lt;/STRONG&gt;&lt;/STRONG&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline"&gt;&lt;img /&gt;&lt;/span&gt;&lt;/P&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/OL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/OL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/BLOCKQUOTE&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;/DIV&gt;&lt;/DIV&gt;</description>
      <pubDate>Fri, 06 Mar 2026 12:19:52 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/building-a-production-style-scd-type-2-dimension-on-delta-lake/m-p/149990#M1057</guid>
      <dc:creator>Prosenjeet33</dc:creator>
      <dc:date>2026-03-06T12:19:52Z</dc:date>
    </item>
  </channel>
</rss>

