Start by modeling SCD Type 2 rows as immutable versions: each change creates a new row for that business entity, and prior rows remain for history.Schema & keys- Natural/business key: e.g., customer_id (identifies the real-world entity).- Surrogate/version key: e.g., customer_sk (unique per row) or generated UUID.- Effective date range: effective_from (inclusive), effective_to (exclusive or null for current).- Status flag / is_current boolean for fast current-row queries.- Optional metadata: source, change_type, load_timestamp, version_number.Storage / architecture- Use a columnar format with ACID/merge support (Delta Lake, Iceberg, Hudi) on the lakehouse.- Partition by a stable dimension (e.g., load_date or hash of business key) and cluster by business key for read performance.- Store base SCD table as parquet/Delta table and maintain incremental ingestion via MERGE/UPSERT.Updates vs inserts (ingest logic)- For each incoming change batch: - Identify incoming rows that differ from latest stored row (compare business key + business attributes). - For changed entities: set existing current row’s effective_to = incoming.effective_from and is_current = false (update). - Insert a new row with effective_from = change_time, effective_to = NULL, is_current = true (insert).- Implement via single transactional MERGE (Delta/Hudi/Iceberg) to atomically update old row(s) and insert new one.- For non-changed rows: do nothing.Example (Delta Lake MERGE pseudo-SQL)sql
MERGE INTO scd_customers T
USING updates S
ON T.customer_id = S.customer_id AND T.is_current = true
WHEN MATCHED AND (T.attr1 <> S.attr1 OR ...) THEN
UPDATE SET T.effective_to = S.effective_from, T.is_current = false
WHEN NOT MATCHED THEN
INSERT (customer_sk, customer_id, attr1, ..., effective_from, effective_to, is_current)
VALUES (uuid(), S.customer_id, S.attr1, ..., S.effective_from, NULL, true)
(You may need two-step MERGE to avoid race conditions: mark old row then insert.)Querying current vs historical- Current row (fast): filter is_current = true OR effective_to IS NULL with partition pruning: SELECT * FROM scd_customers WHERE is_current = true;- State at a point-in-time t: SELECT * FROM scd_customers WHERE effective_from <= t AND (effective_to > t OR effective_to IS NULL);- Daily snapshot for reporting: either run the point-in-time query with t = report_date OR maintain a materialized daily snapshot table (date-partitioned) produced by an incremental job that writes current state each day (cheap reads for reports).- Efficient pattern: keep an indexed/clustered copy of current rows to serve reports; historical queries hit partitioned table using predicates on effective_from/effective_to.Operational concerns- Use CDC or event-driven ingestion to capture changes; deduplicate within micro-batches.- Compact small files and periodically vacuum old file versions.- Retain enough data versions if relying on time-travel.- Monitor merge performance; tune partitioning and file sizes.- Add test coverage for edge cases: out-of-order events, late-arriving rows, identical consecutive updates.This design gives atomic, queryable history, efficient current-row access, and scales on a lakehouse using ACID storage and MERGE semantics.