scd type 2 sql implementation pattern
Gives a standard SQL pattern for Slowly Changing Dimension Type 2. Use it when a data engineer or agent needs to keep full history of dimension changes: expire the old row, insert the new one, track with effective dates and a current flag. Not for Type 1 overwrites, and not for event fact tables.
TL;DR
Type 2 keeps history by never updating in place: each change expires the current row (set end_date and is_current = false) and inserts a new row with a fresh start_date. The standard implementation hashes the tracked columns so change detection is a single comparison, then does it in three steps: insert brand-new keys, expire-and-insert changed rows, leave the rest alone. Get the hash right and the rest is bookkeeping.
The query
scd type 2 sql implementation patternUse this when
- a dimension (customer, product, employee) needs full history of attribute changes
- downstream needs "what was true on date X" queries
- a data agent builds dimension loads and needs a repeatable pattern
Not for
- Type 1 (overwrite, no history) or Type 3 (keep one previous value) dimensions
- fact tables (facts are insert-only; don't SCD them)
- real-time CDC (this is a batch pattern)
Steps
- Add the Type 2 columns to the dimension: a surrogate key,
start_date,end_date(use a far-future date like 9999-12-31 for current rows),is_currentflag, and arow_hashover the tracked attributes.
Expected output: the dimension DDL includes all five, with the current rows flagged.
- In staging, compute the hash over exactly the attributes you track. Exclude metadata like load timestamps or every row will look "changed".
SELECT *, MD5(CONCAT_WS('|', name, email, tier, region)) AS row_hash
FROM staging_customers;Expected output: staging rows carry a hash; identical attribute sets produce identical hashes.
- Insert new natural keys that don't exist in the dimension yet, with
start_date = current_dateandis_current = true.
Expected output: new customers appear as current rows; row counts reconcile with staging.
- For existing keys where the hash differs, expire the old row (
end_date = current_date - 1,is_current = false) and insert the new row as current. Do both in the same transaction.
Expected output: changed customers have exactly one current row and a closed history row; unchanged rows are untouched.
- Validate: no natural key has two current rows, history chains have no date gaps, and a point-in-time query (
WHERE date BETWEEN start_date AND end_date) returns exactly one row per key.
Expected output: all three checks pass, so downstream "as of" queries are trustworthy.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstKJ9oM3MIbUjNYW3zaW2ZQ