Slowly Changing Dimensions: SCD Type 1, 2 and 3 Explained

Slowly changing dimensions explained: SCD Type 1, 2 and 3, effective dates and current flags, surrogate keys, and loading SCD2 tables with MERGE.

Intermediate 5 min read · Updated

A slowly changing dimension (SCD) is a dimension table whose attributes change occasionally: a customer moves city, a product changes category, an employee changes team. The SCD type decides what the warehouse does with the old value. Type 1 overwrites it, Type 2 keeps every version as a separate row, and Type 3 keeps the previous value in an extra column. The choice determines whether last year's report still shows last year's truth.

Customer movesPune to DelhiType 1overwriteType 2close, then insertOne rowhistory lostTwo rowsvalid_from, valid_to
When a customer moves city, Type 1 overwrites the row and loses history; Type 2 closes the old row and adds a new one, so old facts still join to the old city.

The main SCD types

TypeWhat happens on changeHistory keptUse when
Type 0Nothing: the original value is fixedOriginal onlyAttributes that must never change, like a signup date
Type 1Overwrite the value in placeNoneCorrections and attributes where history doesn't matter (a typo in a name)
Type 2Close the current row and insert a new oneFull history, one row per versionReports must reflect the attribute as it was at the time of the fact
Type 3Move the old value to a previous_x columnOne prior valueRare: comparing current vs previous, such as a sales territory realignment
Type 4Move fast-changing attributes to a separate mini-dimension referenced by the factThe profile at each event, without bloating the main dimensionAttributes such as age band, income band or loyalty tier that change often
Type 5Type 4 mini-dimension plus a Type 1 outrigger on the main dimension pointing at the current profileProfile at event time and the current profileYou need both the historical and the current mini-dimension values
Type 6Type 2 rows plus a current-value column updated on every versionFull history and easy current viewReports need both "as it was" and "as it is now" without self-joins
Type 7Fact carries two keys: the Type 2 surrogate key and the durable key, joined to a current-row viewFull history and current view through separate joinsUsers choose per report between "as it was" and "as it is now"

How SCD Type 2 works

Type 2 is the one interviews focus on, because it's the one that keeps history correctly. Each version of an entity gets its own row with its own surrogate key, plus columns describing when that version was valid:

customer_keycustomer_idcityvalid_fromvalid_tois_current
101C-42Pune2024-01-102026-03-31false
187C-42Bengaluru2026-04-019999-12-31true
  • customer_id is the natural key from the source; it's the same across versions.
  • customer_key is the surrogate key; facts store this, so an order placed in 2025 points at the Pune row forever.
  • valid_to of the open row is a far-future date (or null), and is_current makes "current state" queries cheap.
  • Use half-open intervals (valid_from <= t < valid_to) so a timestamp matches exactly one version.

The fact table's foreign key is what makes Type 2 work. When a fact loads, it looks up the dimension version valid at the event time, not the current one. Getting that lookup wrong silently rewrites history.

Loading a Type 2 dimension

Each load compares incoming source rows with the current dimension rows by natural key. Detect a change by comparing tracked columns, often with a hash of them. Then:

  1. New natural key: insert a row with valid_from = effective date and is_current = true.
  2. Tracked attribute changed: set the current row's valid_to to the effective date and is_current = false, then insert the new version.
  3. Only Type 1 attributes changed (for example a corrected email): update in place on the current row (or all rows, by policy).
  4. No change: do nothing.
-- Step 1: close current rows whose tracked attributes changed
merge into dim_customer d
using staged_customers s
  on d.customer_id = s.customer_id and d.is_current
when matched and d.attr_hash <> s.attr_hash then
  update set valid_to = s.effective_date, is_current = false;

-- Step 2: insert a new current version for new or changed customers
insert into dim_customer (customer_key, customer_id, city, attr_hash, valid_from, valid_to, is_current)
select next_key(), s.customer_id, s.city, s.attr_hash, s.effective_date, date '9999-12-31', true
from staged_customers s
left join dim_customer d on d.customer_id = s.customer_id and d.is_current
where d.customer_id is null;

Run the two steps in one transaction, or make the job idempotent so a retry after a failure between them doesn't create duplicate current rows. Assert after every load that each natural key has exactly one current row and that version intervals don't overlap.

Beyond Type 2: types 4, 5, 6 and 7

Type 2 handles history for attributes that change occasionally. Two problems remain: attributes that change too often for a new row each time, and reports that need the current value alongside the historical one. The higher-numbered types are combinations that solve these.

  • Type 4, mini-dimension: volatile attributes (age band, credit score band, loyalty tier) move into a small dimension of banded combinations. The fact references it directly, so each event records the profile at that moment, while the main customer dimension stays stable. See dimension table techniques.
  • Type 5, mini-dimension plus Type 1 outrigger: the base dimension also holds a key to the customer's *current* mini-dimension row, overwritten on change. Reports can group by the profile at the time of the fact or by today's profile.
  • Type 6, the hybrid (1 + 2 + 3): each Type 2 row also carries current-value columns that are overwritten across all versions on every change. A fact joined to its historical row can still show where the customer lives now.
  • Type 7, dual keys: the fact stores the Type 2 surrogate key *and* the entity's durable key. Joining on the surrogate key gives history; joining on the durable key to a view of current rows gives today's values. It is the most flexible option and avoids rewriting old rows, at the cost of two join paths users must understand.
customer_keycustomer_id (durable)city (historical)current_city (Type 6)is_current
101C-42PuneBengalurufalse
187C-42BengaluruBengalurutrue

In interviews, Type 2 is expected; naming Type 4 for fast-changing attributes and Type 6 or 7 for "as it was versus as it is now" reporting shows depth.

Pitfalls

  • Late-arriving data: a change dated last week arrives today. The load must insert the version in the right place in the timeline and possibly re-point facts, not just append.
  • Late-arriving facts: a fact for a date before the current version must join to the version valid then, which is why lookups use event time.
  • Change explosions: tracking a frequently changing attribute (like last_login) creates a new version every day. Keep such attributes out of Type 2 tracking or in a separate fact.
  • Deletes: decide whether a source delete closes the current row, flags it, or is ignored, and handle it explicitly.
  • Source snapshots vs CDC: with full daily snapshots you detect changes by comparison; with change data capture you get each change event, including several in one day, and must decide whether intra-day versions matter.

Where it’s used

Customer analytics

Revenue by customer segment stays correct historically even after customers are re-segmented.

HR reporting

Headcount by department over time uses Type 2 employee rows so transfers appear in the right month.

Product catalogues

Sales by category reflect the category a product had when it was sold, even after a catalogue reorganisation.

In an interview

  • Pick the SCD type per attribute, not per table: Type 1 for corrections, Type 2 for history-relevant attributes.
  • Explain surrogate keys and point-in-time fact lookups; that's what makes Type 2 correct.
  • Describe the MERGE logic and how the load stays idempotent on retry.
  • Raise late-arriving data and deletes before the interviewer does.

Further reading