To implement a daily SCD Type 2 MERGE from staging (stg_customers) into a large dimension (dim_customers) you want: (1) detect new rows -> INSERT, (2) detect changes -> close existing current row (set end_date) and INSERT new current row, (3) no-op when identical. Use a deterministic business key (customer_id) and a change-detection hash (md5 of relevant attributes) to avoid row-by-row comparisons.Approach summary:- Load daily snapshot into a small staging table with computed change_hash and source_load_date.- Compute delta set by joining staging->current active rows only.- Use a single transactional MERGE (Snowflake/Postgres support) or batched updates for very large tables.- Maintain audit/log table for lineage and safe retries.Sample (Snowflake / Postgres-compatible pseudo-SQL):sql
-- 1. prepare staging (already loaded): stg_customers(customer_id, name, email, ... , change_hash, src_load_date)
-- 2. candidate deltas: new inserts and changed rows
WITH current AS (
SELECT id as dim_id, customer_id, change_hash
FROM dim_customers
WHERE current_flag = TRUE
),
delta AS (
SELECT s.*
FROM stg_customers s
LEFT JOIN current c ON s.customer_id = c.customer_id
WHERE c.customer_id IS NULL OR s.change_hash <> c.change_hash
)
-- 3a. For databases supporting MERGE (Snowflake/Postgres 15+)
MERGE INTO dim_customers d
USING delta s
ON d.customer_id = s.customer_id AND d.current_flag = TRUE
WHEN MATCHED THEN
-- close existing current row
UPDATE SET current_flag = FALSE, end_date = s.src_load_date - INTERVAL '1 SECOND'
WHEN NOT MATCHED THEN
-- insert new history row (will also run for matched after update)
INSERT (customer_id, name, email, change_hash, start_date, end_date, current_flag, created_at)
VALUES (s.customer_id, s.name, s.email, s.change_hash, s.src_load_date, NULL, TRUE, CURRENT_TIMESTAMP);
Notes:- In Snowflake, MERGE can run atomically; in Postgres older versions, emulate with explicit TX and UPDATE then INSERT in order (lock rows by business key).- Use change_hash to avoid writes for no-ops and reduce I/O.Efficiency & scaling:- Narrow staging to only changed keys (use CDC or pre-filter).- Sort/partition delta by hash of business key; process in shards (parallel workers).- Use clustering keys (Snowflake) or BRIN/GiST indexes (Postgres) on customer_id/current_flag to speed active-row lookups.- Keep dim table narrow; archive old history to a cold table if too wide.- Use batching (e.g., 100k keys) to avoid long transactions and reduce MVCC bloat in Postgres.- Use COPY/merge-from-temp-table methods to avoid millions of individual statements.Safety & failure handling:- Run inside a transaction where supported; if not, sequence operations: create a staging delta table, UPDATE existing current rows to close (logged), then INSERT new rows. Make steps idempotent by including src_load_date and change_hash and run dedup logic.- Maintain a merge_audit table (batch_id, src_load_date, row_counts, status, error) for retry and reconciliation.- Validate counts: record number of active rows touched vs source delta; alert if mismatch.- For Postgres, use advisory locks or SELECT ... FOR UPDATE on affected keys to avoid concurrency issues.- Test on a representative subset, have backout scripts, and keep backups/snapshots before large runs.Edge cases:- Late-arriving data: handle by backdating start_date and possibly re-splitting history.- Concurrent runs: prevent via locking or single-run scheduler.- Duplicate keys in staging: dedupe by business key keeping latest src_load_date.This pattern minimizes writes (only changed rows), keeps history integrity, and is safe for retries when combined with audit logging and idempotent delta generation.