◐ ai-teacher

❄️ Warehouse & Modeling · Chapter 4

Slowly Changing Dimensions — Type 1, 2, 3

A driver moves from Standard to Premium. Should last year's rides now show up as Premium revenue? Your answer is an SCD type — and picking it silently is how finance and ops end up with different numbers.

Study this lesson free in the app →

The basics

A Slowly Changing Dimension (SCD) is a strategy for what happens to a dimension row when the real-world thing it describes changes — a driver's tier, a rider's home city, a zone's boundary.

Why it exists: facts are history (a ride happened on 3 March), but dimensions describe things that keep changing. If you simply overwrite the driver's tier, every past ride is re-labelled with today's tier, and last year's "Premium revenue" number changes every time a driver is promoted. Sometimes that is what you want. Often it is not.

The mental model: for each attribute, ask "when it changes, should old facts see the old value or the new one?"

Words you must know 5

point-in-time join
linking each fact to the dimension version that was true when the fact happened
surrogate keynatural (business) keyvalid_from / valid_tois_current

Explained

On 1 July, driver Banteilang (DRV-0042) is promoted from Standard to Premium.

On 2 July, the "Premium revenue, H1 2026" number in the board deck goes up by ₹4.1 lakh — for a half-year that already ended.

Nobody changed the rides. Somebody overwrote the driver row. That was a Type 1 decision nobody knew they had made.

The school register

A school keeps one card per student.

Type 1 school: when Daphi moves from Section B to Section A, they rub out B and write A. Last year's sports-day results now say Section A won a race it never ran.

Type 2 school: they file a new card — Daphi, Section A, from 1 July — and stamp the old card valid until 30 June. Old results stay with the old card.

Type 3 school: one card, two boxes: current section A, previous section B. Good enough for "how did the students who switched do?" — useless if she switches again.

No approach is wrong. Each answers a different question.

The three types, precisely

Type 1 — overwrite. UPDATE dim_driver SET tier = 'Premium' WHERE driver_id = 'DRV-0042'. One row per driver. Every fact, past and future, sees Premium. Use for corrections and attributes you never analyse over time (phone number, spelling fixes).

Type 2 — add a row. Expire the current row (valid_to = change_ts, is_current = FALSE) and insert a new one with a new surrogate key, valid_from = change_ts, valid_to = '9999-12-31'. Facts loaded before the change keep the old key; facts after get the new key. History is exact.

Type 3 — add a column. previous_tier, tier_changed_on. One row per driver; you can compare current vs previous only. Use for a planned one-off reclassification (zones remapped into new city boundaries) where people want both views side by side.

Related: Type 0 — never update (signup date). Type 4 — current row in the dim, history in a separate table. Type 6 — Type 2 rows that also carry a Type 1 current_tier column, so you can report by historical or current value from one table.

One promotion, three outcomes

Banteilang promoted Standard → Premium on 2026-07-01

TYPE 1        driver_key  driver_id  tier
              17          DRV-0042   Premium     ← old rides now 'Premium'

TYPE 2        driver_key  driver_id  tier      valid_from  valid_to    is_current
              17          DRV-0042   Standard  2025-02-10  2026-07-01  FALSE
              5121        DRV-0042   Premium   2026-07-01  9999-12-31  TRUE
              ↑ new surrogate key; March rides still point at 17

TYPE 3        driver_key  driver_id  tier     previous_tier  tier_changed_on
              17          DRV-0042   Premium  Standard       2026-07-01

Type 2 with one MERGE

MERGE INTO dim_driver d
USING (
  -- every changed driver appears twice: once to expire, once to insert
  SELECT s.driver_id AS merge_key, s.* FROM stg_driver s
  UNION ALL
  SELECT NULL AS merge_key, s.* FROM stg_driver s
  JOIN dim_driver c ON c.driver_id = s.driver_id AND c.is_current
  WHERE c.row_hash <> s.row_hash            -- tier/city actually changed
) src
ON d.driver_id = src.merge_key AND d.is_current
WHEN MATCHED AND d.row_hash <> src.row_hash THEN UPDATE SET
  valid_to = src.updated_at, is_current = FALSE
WHEN NOT MATCHED THEN INSERT
  (driver_key, driver_id, tier, home_city, row_hash,
   valid_from, valid_to, is_current)
VALUES (dim_driver_seq.nextval, src.driver_id, src.tier, src.home_city,
        src.row_hash, src.updated_at, '9999-12-31', TRUE);

The NULL merge_key copy can never match, so it always hits NOT MATCHED → INSERT. The real-key copy matches the current row and expires it. Brand-new drivers match nothing and insert once. (dbt's snapshot does this for you.)

At 80,000 drivers and 2 billion rides

Gotchas

Type 1 rewrites history. Type 2 records it. Type 3 remembers one step.

Type 2 = new surrogate key + valid_from/valid_to + is_current.

Facts carry the key that was true when the ride happened — half-open interval, 9999-12-31, unknown member -1.

Words to know

SCD Type 1Overwrite the attribute in place; no history, past facts see the new value.
SCD Type 2Insert a new dimension row with a new surrogate key and validity dates for each change; past facts keep the old version.
SCD Type 3Store the previous value in an extra column, giving exactly one level of history.
Natural keyThe business identifier from the source system (DRV-0042), shared by all versions of an entity.
Point-in-time lookupChoosing the dimension version whose validity interval contains the fact's event timestamp.
Half-open intervalvalid_from inclusive, valid_to exclusive, so consecutive versions never overlap.
Type 6Type 2 rows that also carry Type 1 'current' columns, enabling historical and current reporting from one table.

In two minutes

Test yourself

1. Under SCD Type 1, what happens to rides from before a driver's tier change?

  1. They keep the old tier
  2. They show the new tier, because the single row was overwritten
  3. They are deleted
  4. They get a new surrogate key
Show the answer

They show the new tier, because the single row was overwritten — Type 1 overwrites the only row. Every fact joined to it — past or future — now reads the new value. History is rewritten.

2. What does an SCD Type 2 change create?

  1. A previous_value column
  2. A new fact row
  3. A new dimension row with a new surrogate key, while the old row is closed with valid_to
  4. An audit log in a separate database
Show the answer

A new dimension row with a new surrogate key, while the old row is closed with valid_to — Type 2 adds a version. The new surrogate key is essential — facts after the change point at it, facts before keep the old key.

3. When is SCD Type 3 the right choice?

  1. A one-off reclassification where people want 'current vs previous' side by side
  2. Tracking every price change of a product
  3. Fixing typos in names
  4. Any attribute that changes daily
Show the answer

A one-off reclassification where people want 'current vs previous' side by side — Type 3 stores exactly one prior value — perfect for a planned remap (old zone boundaries vs new), useless for repeated changes.

4. A ride from 28 June arrives late, on 5 July. The driver was promoted on 1 July (Type 2). Which dimension row should the fact get?

  1. The is_current row, since that's what the dim says today
  2. Whichever row has the higher surrogate key
  3. The row whose [valid_from, valid_to) contains 28 June
  4. Neither — late facts are dropped
Show the answer

The row whose [valid_from, valid_to) contains 28 June — Point-in-time lookup on event time, not load time. Using is_current would label a Standard-tier ride as Premium.

5. Current Type 2 rows have valid_to = NULL. A dashboard filters ride_ts BETWEEN valid_from AND valid_to. What happens?

  1. It works fine
  2. Every ride matched to a current version is dropped, because comparisons with NULL are not true
  3. Old rides are dropped
  4. Rides are double counted
Show the answer

Every ride matched to a current version is dropped, because comparisons with NULL are not true — x <= NULL is NULL, never true, so current-version rides fall out. That's why open-ended rows use '9999-12-31'.

6. The driver dimension tracks tier (Type 2) and phone number. Drivers change phones often. What should phone be?

  1. Type 2, for completeness
  2. Type 3
  3. Type 0
  4. Type 1, excluded from the change-detection hash
Show the answer

Type 1, excluded from the change-detection hash — Nobody analyses revenue by historical phone number. Making it Type 2 would create a version per phone change and bloat the dimension.

There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →