Approach (summary)Keep stable descriptive attributes denormalized in the star dimension for fast queries, but move high-churn attributes into a normalized change-history table (SCD Type 2). Maintain a denormalized "snapshot" dimension table for BI queries that is refreshed incrementally from the normalized history so dashboards get fast reads while historical changes are preserved.Schema (concept)- dim_customer (denormalized snapshot for BI) - customer_id (pk), name, email, current_status, churn_flag, last_updated, ...- hist_customer_status (normalized SCD Type 2) - hist_id, customer_id, status, effective_from, effective_to, is_current, other_metaPattern 1 — Insert new change into history (SCD Type 2)sql
INSERT INTO hist_customer_status (customer_id, status, effective_from, effective_to, is_current)
VALUES (:cust_id, :new_status, CURRENT_TIMESTAMP, '9999-12-31', TRUE);
-- mark previous as not current
UPDATE hist_customer_status
SET is_current = FALSE, effective_to = CURRENT_TIMESTAMP
WHERE customer_id = :cust_id AND hist_id <> (SELECT MAX(hist_id) FROM hist_customer_status WHERE customer_id = :cust_id AND is_current = TRUE);
Pattern 2 — Upsert/refresh denormalized snapshot (incremental)Use MERGE (SQL Server/Oracle) or INSERT ... ON CONFLICT (Postgres) to keep snapshot fast and consistent:sql
MERGE INTO dim_customer d
USING (
SELECT h.customer_id, h.status AS current_status, max(h.effective_from) AS last_updated
FROM hist_customer_status h
WHERE h.is_current = TRUE
GROUP BY h.customer_id, h.status
) src
ON (d.customer_id = src.customer_id)
WHEN MATCHED AND d.current_status <> src.current_status THEN
UPDATE SET current_status = src.current_status, last_updated = src.last_updated
WHEN NOT MATCHED THEN
INSERT (customer_id, name, current_status, last_updated)
VALUES (src.customer_id, NULL, src.current_status, src.last_updated);
Querying guidance- BI queries use dim_customer for joins and speed.- For time-travel or event history, join fact tables to hist_customer_status by valid date range:sql
SELECT f.*, h.status
FROM fact_orders f
JOIN hist_customer_status h
ON f.customer_id = h.customer_id
AND f.order_date BETWEEN h.effective_from AND h.effective_to;
Operational notes & trade-offs- Refresh frequency: near-real-time if low volume; batch hourly/daily for heavy writes.- Use CDC or change tables to capture churn events.- Keep snapshot minimal (only current, commonly queried attributes) to reduce update churn.- Auditability: hist_customer_status retains full SCD2 history; dim_customer is purely a performance layer and can be rebuilt from history if needed.