Data Warehousing Updated: 2026-04-03

What is Slowly Changing Dimensions (SCD)?

A concept in data warehousing to manage how data that changes slowly over time is stored and tracked.

From the field

Slowly Changing Dimensions (SCDs) are the pattern that separates "a warehouse that reports the truth at this instant" from "a warehouse that reports the truth as of any point in history." Every dimension attribute — customer address, employee department, product category, sales territory — drifts over time. Your warehouse has to decide: when the CRM updates a customer's city from Hyderabad to Bangalore, does every historical order for that customer now show Bangalore (Type 1, overwrite), or do orders before the move show Hyderabad and after show Bangalore (Type 2, versioned history)? That single decision ripples through every report, every ML feature, and every compliance audit you'll ever run.

When SCD decisions actually bite

The SCD choice feels academic until you hit one of these: (1) regional revenue reports that retroactively shift after a sales territory reorg, (2) ML training sets that leak future information into historical features, (3) auditors asking "what did this customer's KYC status look like on the day of the transaction," (4) churn analysis where "customer segment" keeps changing so cohort definitions drift. Type 1 is fine for corrections (fixing a misspelled name) but lethal for any attribute that influences metrics or ML. Type 2 is the right default for most business-meaningful dimensions in a Kimball-style warehouse.

Field story

A B2B SaaS client had 3 years of Type 1 dimensions in Redshift — everything overwritten, no history. When their sales org restructured regions, the quarterly board deck suddenly showed different regional revenue numbers for prior quarters than had been presented before. The CFO was livid. We spent 4 months retrofitting SCD Type 2 onto 12 core dimensions (customer, account, salesperson, territory, product). The hard part wasn't the dbt snapshot logic — it was backfilling history we didn't have (had to reconstruct from Salesforce audit logs) and updating 200+ downstream models to join on valid_from/valid_to with point-in-time correctness. Lesson: pick your SCD strategy before the first dashboard goes to an exec; retrofitting is 10x the work of doing it right up front.

Slowly Changing Dimensions (SCD) are a set of techniques for tracking historical changes in dimension tables within a data warehouse. When attributes of business entities change over time — a customer moves to a new city, an employee gets a new title, a product changes its price tier — you need a strategy for how the warehouse reflects those changes.

Why SCDs Matter

Without SCD strategies, your warehouse loses historical context:

  • Analytics break: 'Revenue by region' changes retroactively if you just overwrite
  • Audit requirements: Regulators may require you to show what data looked like at a point in time
  • ML models: Training data needs point-in-time accuracy to avoid data leakage

SCD Types Explained

Type 0 — Retain Original

The attribute value never changes in the warehouse, even if it changes in the source.
  • Use when: The value should remain as it was at the time of initial load (e.g., original sign-up date)
  • Pros: Simple, no logic needed
  • Cons: Warehouse diverges from source

Type 1 — Overwrite

Replace the old value with the new value. No history is kept.
UPDATE dim_customer
SET city = 'Bangalore'
WHERE customer_id = 101;
  • Use when: Historical values are not needed (e.g., correcting a typo)
  • Pros: Simple, no extra storage
  • Cons: Loses all history

Type 2 — Add New Row (Most Common)

Insert a new row for each change, with validity dates to track history.
-- Before
| customer_id | name  | city       | valid_from | valid_to   | is_current |
|-------------|-------|------------|------------|------------|------------|
| 101         | Rahul | Hyderabad  | 2024-01-01 | 9999-12-31 | true       |

-- After city change
| customer_id | name  | city       | valid_from | valid_to   | is_current |
|-------------|-------|------------|------------|------------|------------|
| 101         | Rahul | Hyderabad  | 2024-01-01 | 2026-03-15 | false      |
| 101         | Rahul | Bangalore  | 2026-03-15 | 9999-12-31 | true       |
  • Use when: Full history tracking is required
  • Pros: Complete audit trail, point-in-time queries
  • Cons: Table grows over time, joins need valid_from/valid_to logic

Type 3 — Add New Column

Keep the previous value in a separate column.
| customer_id | name  | current_city | previous_city |
|-------------|-------|-------------|---------------|
| 101         | Rahul | Bangalore   | Hyderabad     |
  • Use when: You only need one level of history
  • Pros: Simple to query
  • Cons: Only tracks one prior value

Type 4 — History Table

Keep the current record in the main dimension and store all historical changes in a separate history table.
  • Use when: You want fast lookups on current data while keeping full history accessible

Type 6 — Hybrid (1 + 2 + 3)

Combines Type 1, 2, and 3: add new rows (Type 2), keep a current-value column updated across all rows (Type 1), and store previous values (Type 3).
  • Use when: You need both historical and current values easily accessible

Implementing SCD Type 2 in dbt

dbt has a built-in snapshot feature for SCD Type 2:

# models/snapshots/customer_snapshot.sql
{% snapshot customer_snapshot %}
  {{ config(
    target_schema='snapshots',
    unique_key='customer_id',
    strategy='timestamp',
    updated_at='updated_at'
  ) }}
  SELECT * FROM {{ source('raw', 'customers') }}
{% endsnapshot %}

Choosing the Right SCD Type

RequirementRecommended Type
No history neededType 1
Full audit trailType 2
Only previous valueType 3
Fast current lookups + historyType 4 or Type 6
Immutable attributesType 0

Key Points

Gotchas

SCD implementation traps that cause silent data corruption:

  • Silently overwriting Type 2 history. An ELT job MERGE that doesn't check for changes in SCD Type 2 attributes will update the current row instead of closing it and inserting a new one. Always use explicit change-detection logic (dbt snapshot, or custom MERGE with WHEN MATCHED AND clauses).
  • Point-in-time join bugs. Fact table joined to dim on surrogate key is fine; fact joined to dim on natural key + date range needs BETWEEN valid_from AND valid_to — getting the boundary conditions wrong (using <= vs <) duplicates or drops rows. Always test with a customer who changed on a boundary date.
  • Open-ended valid_to = '9999-12-31' vs NULL inconsistency. Pick one convention and enforce it. Mixing both in the same table breaks WHERE valid_to > CURRENT_DATE and WHERE valid_to IS NULL filters simultaneously.
  • Multiple current rows per natural key. A bug in the snapshot logic can leave two rows with is_current = true for the same customer. Add a uniqueness DMF or test that flags this immediately; if it accumulates, every aggregation doubles.
  • Fast-changing dimensions disguised as slow. Session IDs, sensor readings, or pricing that changes hourly shouldn't be Type 2 dimensions — they'll blow up the table size. These need a separate mini-dimension or a fact-table-like design (Type 4).
  • Type 2 without business-date accuracy. valid_from based on load timestamp, not business event timestamp, makes point-in-time joins wrong when the source system delivers changes late. Use the source's updated_at when available.
  • Surrogate key reuse after Type 2 update. If you rebuild the dimension and reassign surrogate keys, every fact table that joined to old keys is now pointing at wrong rows. Surrogate keys must be stable across rebuilds.
  • Forgetting that Type 1 updates break audit trails. Even a seemingly innocent Type 1 field (email address) can matter for compliance. If the regulator asks "what email was on file when this transaction happened," Type 1 can't answer.
  • dbt snapshots on wrong strategy. strategy='timestamp' requires a reliable updated_at column; strategy='check' compares column values but gets slow on wide tables. Pick based on source reliability, not convenience.
  • Type 2 row explosion from noisy source columns. Including last_login_at as a Type 2 tracked column creates a new row every login. Only track columns that have business meaning for historical analysis.

Frequently Asked Questions

What is SCD Type 2 in a data warehouse?

SCD Type 2 inserts a new row in the dimension table every time one or more tracked attributes change, marking the old row as expired and leaving the new row as current. The row includes metadata columns — typically `valid_from`, `valid_to` (or `NULL`/`'9999-12-31'` for the current row), `is_current` flag, and often a version number. This preserves complete history: you can answer "what was this customer's segment on July 15th, 2024" by filtering `WHERE valid_from <= '2024-07-15' AND (valid_to > '2024-07-15' OR valid_to IS NULL)`. Type 2 is the default for any dimension attribute that influences metrics, ML features, or compliance reporting. The cost: table grows with each change, and fact-table joins become date-range joins rather than simple equi-joins.

When should I use SCD Type 1 vs Type 2?

Type 1 (overwrite, no history) is appropriate only when (1) the previous value has no analytical or compliance meaning — fixing typos, correcting data entry errors, updating coordinates when lat/long precision improves; (2) the attribute is descriptive and never appears in metric definitions or filters — a customer's preferred-name field displayed in UIs but never aggregated on. Type 2 is the correct default for every other case: customer segment, sales territory, product category, employee department, pricing tier, compliance status. The cost of Type 2 (extra storage, slightly more complex joins) is trivial compared to the cost of losing history. When in doubt, go Type 2 — you can always query the current state with `WHERE is_current = true`, but you can't recover history you threw away.

How do you implement SCD Type 2 in dbt?

dbt's `snapshot` block automates Type 2: point it at a source model, declare `unique_key` (natural key) and `strategy` (`timestamp` or `check`), and dbt generates the logic to insert new versions when changes are detected. For `strategy='timestamp'`, specify an `updated_at` column; dbt compares values and inserts a new row when the source timestamp is newer. For `strategy='check'`, specify `check_cols` (array of columns or `all`); dbt compares column values directly. Run `dbt snapshot` on your orchestration schedule (typically hourly or daily). dbt manages `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id` automatically. For complex cases (soft deletes, composite keys, backfills) you may need custom materializations, but `snapshot` handles ~80% of real-world Type 2 needs with a dozen lines of YAML.

What is SCD Type 6?

SCD Type 6 is a hybrid ("1 + 2 + 3 = 6") that combines overwrite, versioning, and current-value tracking in one table. Each row has: (a) the historical value of the attribute at that row's validity window (Type 2 behavior), (b) a `current_<attr>` column that's updated across ALL rows for that natural key whenever the attribute changes (Type 1 behavior applied to a mirror column), and (c) optionally a `previous_<attr>` column showing the value immediately before (Type 3). Type 6 lets you query "current customer segment as of today" and "historical customer segment at order time" from the same dimension without a self-join. The cost: updates cascade across all historical rows for a natural key, making writes slower. Use when reporting requires both current-state and point-in-time views with low-latency.

What are the other SCD types (0, 3, 4, 7)?

Type 0: retain original, never update — suitable for values that should freeze at initial load (original account opening date). Type 3: add columns for previous values (`current_region`, `previous_region`, `region_change_date`) — captures one step of history; useful when you only care about the most recent change but not the full chain. Type 4: split rapidly-changing attributes into a separate "mini-dimension" table, while the main dimension uses Type 1 or 2 — avoids Type 2 row explosion from noisy attributes. Type 7: dual Type 1 + Type 2 tables (or two columns pointing to current and historical surrogate keys) — gives consumers flexibility to choose current or historical joins without applying Type 6 logic. In practice, Type 1 and Type 2 handle 95% of needs; Type 4 and 6 appear in large-scale Kimball warehouses.

How do SCDs affect fact table joins?

With Type 1 dimensions, facts join on natural or surrogate key — simple equi-join. With Type 2, facts must join on the surrogate key that represents the dimension version valid at the fact's event date. Two correct patterns: (1) at ETL time, look up the surrogate key from the Type 2 dimension using fact event date between `valid_from` and `valid_to`, then store that surrogate key on the fact row — joins are simple but backfills require recomputing surrogate keys if dimensions change; (2) store natural key on the fact and do date-range joins at query time (`fact.customer_id = dim.customer_id AND fact.event_date BETWEEN dim.valid_from AND dim.valid_to`) — more flexible but slower and easier to get wrong. Pattern (1) is preferred for high-scale fact tables; pattern (2) works for ad-hoc analytics.

What is a point-in-time (PIT) table and when do you need one?

A point-in-time (PIT) table pre-materializes surrogate-key lookups for an "as-of" date, letting fact tables join to multiple Type 2 dimensions without date-range gymnastics. It's common in Data Vault 2.0 architectures where satellites (equivalent to Type 2 dimensions) are used heavily. For each as-of date you care about (daily snapshots, end-of-month, etc.), the PIT table stores the surrogate key from each satellite that was current on that date. Queries join fact → PIT on business key + as-of date → dimensions on surrogate key, turning N date-range joins into N equi-joins. Needed when: (1) you have many Type 2 dimensions to join, (2) point-in-time queries are common, (3) query latency matters. Overkill for simple star schemas with 2-3 Type 2 dimensions.

Related Terms

Data WarehouseData ModelingStar SchemaDimensional Modelingdbt (Data Build Tool)

Related Tools

dbtInformaticaSnowflakeBigQueryApache Spark

Learn More

Browse All Terms Read Articles