Slowly Changing Dimensions
How to handle dimension attributes that change over time in a Power BI model, using Type 1 and Type 2 slowly changing dimension patterns.
Slowly Changing Dimensions
A slowly changing dimension (SCD) is a dimension whose attributes change occasionally rather than never — a customer moves region, a product gets reassigned to a new category, an employee changes department.
DimCustomer
CustomerKey | CustomerName | Region
1001 | Alice Smith | WestThe question an SCD strategy answers: when that row changes, should the history behind it change too?
Type 0: Fixed, No Changes
The attribute never changes after the row is created — a date of birth, an original signup date.
No updates. The value written once stays as-is forever.No special handling is needed; this is the default for anything that genuinely can't change.
Type 1: Overwrite
The old value is simply replaced with the new one, and history is not preserved.
Before: After Alice moves to East:
CustomerKey | Region CustomerKey | Region
1001 | West 1001 | EastEvery past and future report using this dimension now shows Alice in the East, even for sales that happened while she was in the West.
FactSales (unchanged)
|
| joins to
|
DimCustomer[CustomerKey] = 1001, Region = East (always, even for old sales)Type 1 is simple and appropriate when historical accuracy for that specific attribute doesn't matter — correcting a misspelled name, for example.
Type 2: Preserve History with New Rows
Instead of overwriting the row, a new row is added, and the old row is marked as no longer current.
CustomerKey | CustomerName | Region | EffectiveDate | ExpiryDate | IsCurrent
1001 | Alice Smith | West | 2020-01-01 | 2024-06-01 | No
1002 | Alice Smith | East | 2024-06-01 | (null) | YesTwo structural changes make this work:
- A surrogate key (
CustomerKey) that's independent of the business key (Alice's real-world identity), so the same person can have multiple dimension rows. - Effective/expiry dates (or an
IsCurrentflag) marking which row was valid at any point in time.
FactSales (DateKey, CustomerKey)
|
| each historical sale references
|
the CustomerKey that was current on the sale's dateA sale made in 2022 references CustomerKey = 1001 (West), and a sale made in 2025 references CustomerKey = 1002 (East) — history stays accurate to how things were at the time.
Anatomy of Type 1 vs. Type 2: The Same Change, Two Outcomes
Alice Smith moves from West to East on 2024-06-01. FactSales has sales from both before and after that date, all originally referencing CustomerKey 1001.
2Type 1 — Overwrite
| CustomerKey | CustomerName | Region |
|---|---|---|
| 1001 | Alice Smith | West |
| 1001 | Alice Smith | East |
The same row, same key — the old value is gone. Every FactSales row that joins to CustomerKey 1001 now shows East, including sales made in 2022 while Alice was still in the West. History gets silently rewritten.
3Type 2 — New Row
| CustomerKey | Region | EffectiveDate | ExpiryDate | IsCurrent |
|---|---|---|---|---|
| 1001 | West | 2020-01-01 | 2024-06-01 | No |
| 1002 ✓ | East | 2024-06-01 | — | Yes |
A new surrogate key (1002) for the same person. A 2022 sale still references CustomerKey 1001 (West), exactly as it did at the time; a 2025 sale references 1002 (East). History stays accurate to when things actually happened.
4Same real-world event, two different histories left behind
The change — one real-world event (Alice's region change) is the input to both strategies below.
Type 1 — simplest to implement, but every past fact row inherits the new value. Appropriate when historical accuracy for that attribute doesn't matter, like fixing a misspelled name.
Type 2 — requires a surrogate key and effective/expiry dates, but each fact row keeps pointing at the dimension row that was actually current when it happened.
The tradeoff — Type 1 costs nothing extra but loses history; Type 2 preserves history but grows the dimension table and needs the fact table to reference the right surrogate key at load time.
Type 3: Track Limited History in Columns
Rather than adding rows, a Type 3 dimension adds a column to hold the previous value.
CustomerKey | Region | PreviousRegion
1001 | East | WestThis only tracks one prior state, and is used far less often than Type 1 or Type 2 — mostly when a single "what was it before" comparison is enough, and full history isn't needed.
Choosing a Type
Does the attribute's history matter for reporting?
|
+-- No -> Type 1 (overwrite)
|
+-- Yes -> Type 2 (new row per change)
|
+-- Only need the immediately prior value? -> Type 3Most enterprise models use Type 1 for corrections and low-impact attributes, and Type 2 for anything that affects how past transactions should be grouped or filtered — like sales territory or customer segment.
Implementing Type 2 in Power Query
Detecting a changed row typically means comparing the incoming source row against the current dimension row, and inserting a new row — with a new surrogate key and updated effective date — whenever a tracked attribute differs. This logic usually lives in the ETL/Power Query layer, not in DAX. DAX consumes the resulting historized table; it doesn't create the history.
Best Practices
- Use a surrogate key, not the business key, whenever a dimension needs Type 2 history — the business key alone can't represent "this customer, but as they were before."
- Only apply Type 2 to attributes where historical accuracy actually matters for reporting; applying it to every attribute bloats the dimension table with rows that don't add analytical value.
- Store effective and expiry dates (or a clear
IsCurrentflag) so "what was true on this date" can always be answered. - Keep the fact table's foreign key pointing at the surrogate key that was current when the transaction happened, not the current row.
Common Mistakes
Using Type 1 When History Matters
Overwriting a region or segment attribute that reports are sliced by silently rewrites history — a customer who moved regions last month now appears to have always been in the new region.
Using the Business Key Instead of a Surrogate Key
Without a surrogate key, there's no way to have two rows represent "the same customer, at two different points in time."
Applying Type 2 to Everything
Tracking full history for attributes nobody analyzes historically, like a phone number, just grows the dimension table and adds unnecessary complexity.
Slowly Changing Dimension Checklist
- Each attribute has a deliberate choice of Type 0, 1, 2, or 3 — not a default applied blindly.
- Type 2 dimensions use a surrogate key, separate from the business key.
- Type 2 dimensions store effective/expiry dates or an
IsCurrentflag. - Fact tables reference the surrogate key that was current at the time of the transaction.
Next Steps
Continue learning Power BI data modeling: