Slowly changing dimensions (SCD) help track changes in data over time. Type 2 keeps full history by adding new records when data changes.
Slowly changing dimensions (SCD Type 2) in dbt
with source_data as ( select * from {{ ref('your_source_table') }} ), new_records as ( select sd.id, sd.attribute, current_timestamp as effective_from, null as effective_to, true as is_current from source_data sd left join {{ this }} t on sd.id = t.id and t.is_current = true where t.id is null or sd.attribute <> t.attribute ), expired_records as ( select t.id, t.attribute, t.effective_from, current_timestamp as effective_to, false as is_current from {{ this }} t join source_data sd on t.id = sd.id where t.is_current = true and sd.attribute <> t.attribute ), final as ( select * from {{ this }} where is_current = false union all select * from expired_records union all select * from new_records union all select * from {{ this }} where is_current = true and id not in (select id from expired_records) ) select * from final
This example shows the main steps: find new or changed records, expire old ones, and keep current data.
Use {{ ref('table_name') }} to refer to source tables and {{ this }} for the current model.
select * from source_tableselect * from target_table where is_current = trueupdate target_table set effective_to = current_timestamp, is_current = false where id = '123' and is_current = true
insert into target_table (id, attribute, effective_from, effective_to, is_current) values ('123', 'new_value', current_timestamp, null, true)
This dbt model updates a customer dimension table using SCD Type 2 logic. It finds new or changed customers, expires old records, and inserts new current records.
with source_data as ( select * from {{ ref('customers_source') }} ), new_records as ( select sd.customer_id, sd.customer_name, current_timestamp as effective_from, null as effective_to, true as is_current from source_data sd left join {{ this }} t on sd.customer_id = t.customer_id and t.is_current = true where t.customer_id is null or sd.customer_name <> t.customer_name ), expired_records as ( select t.customer_id, t.customer_name, t.effective_from, current_timestamp as effective_to, false as is_current from {{ this }} t join source_data sd on t.customer_id = sd.customer_id where t.is_current = true and sd.customer_name <> t.customer_name ), final as ( select * from {{ this }} where is_current = false union all select * from expired_records union all select * from new_records union all select * from {{ this }} where is_current = true and customer_id not in (select customer_id from expired_records) ) select * from final
Always include effective_from and effective_to timestamps to track when records are valid.
Mark current records with a boolean flag like is_current for easy filtering.
Test your SCD logic on a small dataset before applying to large tables.
SCD Type 2 keeps full history by adding new rows for changes.
Use timestamps and flags to track current and past records.
dbt models can implement SCD Type 2 with SQL and incremental logic.
Practice
Solution
Step 1: Understand SCD Type 2 concept
SCD Type 2 tracks changes by adding new rows instead of overwriting existing data.Step 2: Compare options with concept
Only To keep full history of changes by adding new rows for each change describes adding new rows to keep full history, which matches SCD Type 2.Final Answer:
To keep full history of changes by adding new rows for each change -> Option AQuick Check:
SCD Type 2 = Add new rows for history [OK]
- Confusing SCD Type 2 with Type 1 which overwrites data
- Thinking SCD deletes old records
- Assuming SCD aggregates data
Solution
Step 1: Identify incremental loading condition
Incremental models use a WHERE clause to filter new or changed rows based on a timestamp.Step 2: Match clause to SCD Type 2 incremental logic
WHERE updated_at > (SELECT MAX(updated_at) FROM target_table) filters rows with updated_at greater than the max in the target, enabling incremental load.Final Answer:
WHERE updated_at > (SELECT MAX(updated_at) FROM target_table) -> Option DQuick Check:
Incremental WHERE clause = WHERE updated_at > (SELECT MAX(updated_at) FROM target_table) [OK]
- Using GROUP BY instead of filtering new rows
- Ordering data without filtering
- Using HAVING without aggregation context
SELECT id, name, effective_from, effective_to, is_current FROM customer_scd WHERE is_current = TRUE
What will this query return?
Solution
Step 1: Analyze WHERE clause filtering
The query filters rows where is_current = TRUE, meaning only the latest active records.Step 2: Understand SCD Type 2 flags
In SCD Type 2, is_current marks the latest version; older versions have FALSE.Final Answer:
Only the latest active records for each customer -> Option CQuick Check:
is_current = TRUE returns latest records [OK]
- Assuming it returns all history
- Confusing effective_to NULL with is_current TRUE
- Ignoring the is_current flag meaning
WITH new_data AS ( SELECT * FROM source_table WHERE updated_at > (SELECT MAX(updated_at) FROM target_table) ), updated_records AS ( SELECT * FROM target_table WHERE is_current = TRUE ) SELECT * FROM updated_records UNION ALL SELECT * FROM new_data
What is the main issue causing duplicates?
Solution
Step 1: Understand SCD Type 2 update process
Old current records must be marked expired (is_current = FALSE) before adding new versions.Step 2: Identify missing expiration step
The query selects current records but does not update their is_current flag to FALSE, causing duplicates.Final Answer:
Missing logic to expire old current records before inserting new ones -> Option BQuick Check:
Expire old records before insert = Missing logic to expire old current records before inserting new ones [OK]
- Thinking UNION ALL causes duplicates here
- Believing WHERE clause is wrong without checking logic
- Assuming distinct needed without expiration logic
Solution
Step 1: Identify key SCD Type 2 columns
SCD Type 2 needs effective_from and effective_to timestamps plus a flag like is_current to mark active rows.Step 2: Compare options for completeness
customer_id, name, effective_from, effective_to, is_current includes customer_id, name, effective_from, effective_to, and is_current, covering history and current record tracking.Final Answer:
customer_id, name, effective_from, effective_to, is_current -> Option AQuick Check:
Effective dates + is_current = customer_id, name, effective_from, effective_to, is_current [OK]
- Using only timestamps without current flag
- Confusing deleted_flag with is_current
- Using version_number without effective dates
Ready to prove it?
Pass the test and this pattern moves unknown
