Type 1 — Overwrite

scd-type-1.sql
UPDATE dim_customer
SET email = 'new.email@example.com'
WHERE customer_id = 1042;
-- The old email is gone permanently — no way to recover it from this table

Simplest possible approach: the row is updated, the old value is gone. Correct for attributes where history genuinely has no value — fixing a typo, updating a phone number where no report ever needs to know the old one.

Advertisement

Type 2 — New row per change

scd-type-2.sql
UPDATE dim_customer SET expiration_date = CURRENT_DATE, is_current = FALSE
WHERE customer_id = 1042 AND is_current = TRUE;

INSERT INTO dim_customer (customer_id, region, effective_date, expiration_date, is_current)
VALUES (1042, 'EMEA', CURRENT_DATE, NULL, TRUE);
-- Old fact rows still join correctly to the historical 'is_current = FALSE' version

Full history preserved, and any historical fact still joins to the dimension state that was actually true when the fact occurred. This is the most detailed treatment covered here — see the dedicated SCD Type 2 guide for surrogate key design and a full MERGE-based implementation.

Type 3 — Previous-value column

scd-type-3.sql
ALTER TABLE dim_customer ADD COLUMN previous_region VARCHAR(50);

UPDATE dim_customer
SET previous_region = current_region,
    current_region  = 'EMEA'
WHERE customer_id = 1042;
-- Only one prior state is retained — a second change overwrites previous_region too

A middle ground: one "before" value is kept alongside the current one, enough for a before/after comparison, but a second change to the same attribute discards whatever was in previous_region — there's no way to represent three or more historical states this way.

Type 4 — Separate history table

scd-type-4.sql
CREATE TABLE dim_customer (            -- current values only, stays small
  customer_id INT PRIMARY KEY,
  region      VARCHAR(50)
);

CREATE TABLE dim_customer_history (    -- every version, grows over time
  history_id     BIGINT PRIMARY KEY,
  customer_id    INT,
  region         VARCHAR(50),
  effective_date DATE,
  expiration_date DATE
);

-- On a change: UPDATE dim_customer with the new value,
-- INSERT the outgoing state into dim_customer_history

The main dimension table stays lean — every query that only needs current values avoids scanning historical rows entirely. The tradeoff is an extra table to maintain and an extra join whenever a report genuinely needs point-in-time history.

Advertisement

Side-by-side comparison

Type 1Type 2Type 3Type 4
History keptNoneFullOne prior valueFull, separate table
Extra table neededNoNoNoYes
Main table growthNoneGrows with every changeNoneStays small
Query complexitySimplestModerate (is_current filter)SimpleExtra join for history
Best forCorrections, no-history attributesAttributes needing full audit trailSimple before/after comparisonsHigh-frequency changes, lean current table

Choosing the right type per attribute

SCD type is a per-attribute decision, not a whole-table one — a single customer dimension might reasonably use Type 1 for a corrected email typo, Type 2 for a customer-tier change that affects historical revenue attribution, and Type 3 for a simple current-vs-previous comparison on a different field. Ask, for each attribute: does a historical fact need to see the value that was true at the time, or only the value as it is now?

Common mistakes

  • Defaulting to Type 1 for an attribute that actually needs audit history — a compliance requirement or a "what did this customer's tier look like last quarter" report can't be answered after the fact.
  • Applying Type 2 to an attribute that changes extremely frequently, causing the dimension table to grow far faster than expected — Type 4 is usually the better fit there.
  • Forgetting to chain Type 3's previous-value column correctly across more than one change, silently losing the value that was in "previous" before the newest update.
  • Skipping an index on the history table's foreign key in Type 4, making point-in-time history lookups far slower than they need to be.

Key takeaways

  • Type 1 overwrites with no history — fine for corrections, wrong for anything needing an audit trail.
  • Type 2 preserves full history in the same table via new rows — the most common default for historically significant attributes.
  • Type 3 keeps exactly one prior value — enough for a simple before/after, not for multi-step history.
  • Type 4 offloads history to a separate table, keeping the main dimension small for high-frequency changes.
  • The SCD type decision applies per attribute, not to the whole dimension table at once.