System Design · medium

How do you model a Type 2 slowly changing dimension, and how do you test it?

Asked in Analytics Engineer interviews, in the System Design round.

Short answer

One row per version of the entity with valid_from / valid_to and a current flag, and a surrogate key per version so facts join to the state as of the event. Test that exactly one version is current per natural key, that the validity windows never overlap, and that valid_from < valid_to, dbt unique/not_null plus a custom overlap test.

How to answer it

Definition

A Type 2 dimension keeps history by writing a new row each time a tracked attribute changes, rather than overwriting. Each row is valid for a window of time, so you can answer what was true when something happened.

The shape

customer_key   surrogate, unique per VERSION
customer_id    natural key, repeats across versions
segment        the tracked attribute
valid_from     inclusive
valid_to       exclusive, far-future sentinel for the open row
is_current     convenience flag, derivable from valid_to
customer_key  customer_id  segment    valid_from   valid_to     is_current
      90001          4471  Standard   2025-01-01   2025-03-01        false
      90002          4471  Premium    2025-03-01   9999-12-31        true

How it updates

Compare incoming rows against the current version. Where a tracked attribute differs, close the old row by setting valid_to to now and insert a new row with valid_from now and the sentinel valid_to. In dbt this is a snapshot, with a strategy of either check on named columns or timestamp on an updated_at column.

Decide deliberately which columns are tracked. Making every column Type 2 means a new version whenever a phone number is corrected, and the table grows without adding analytical value.

How to test it

1. No overlapping intervals per natural key
     two versions of one customer must not both cover the same instant

2. No gaps
     the intervals for one customer are contiguous from first to last

3. Exactly one current row per natural key
     the count where is_current is true must equal 1

4. is_current agrees with valid_to
     true if and only if valid_to is the sentinel

5. valid_from < valid_to on every row

6. Fact row count unchanged after joining
     the range join must not duplicate or drop facts

Tests 1 and 3 are the ones that catch real bugs. The overlap test in particular is what stops a fact joining to two versions and double counting.

Key points

FAQ

The 30-second answer

Each change to a tracked attribute writes a new row rather than overwriting, with valid_from and valid_to marking the window it applied to, an is_current flag, and a surrogate key unique per version rather than per customer. I would use half-open intervals, valid_from inclusive and valid_to exclusive, and a far-future sentinel rather than NULL on the open row.
In dbt that is a snapshot, and I would choose the tracked columns deliberately, because making everything Type 2 creates a new version every time a phone number is corrected.
For testing, the three that catch real bugs are no overlapping intervals for a natural key, no gaps between them, and exactly one current row per key. I would also assert that joining a fact table does not change the fact row count, since an overlap shows up as double counted revenue.

In the interview

Practice this for real