Introduction
Slowly Changing Dimensions (SCDs) are a common data modeling technique used to manage historical changes in dimension data over time. This enables more accurate time-based analysis and reporting, such as understanding how KPIs were affected under previous attribute values. SCDs are categorized into different types based on how they handle changes to dimension data:SCD Type 0 - Fixed Dimensions
- No changes allowed. The data remains as it was when first inserted.
- Useful when historical accuracy is critical and the value should never change.
- Example: A product’s original launch date.
SCD Type 1 - Overwrite
- Changes overwrite existing data. No history is preserved.
- Simple to implement but loses historical context.
- Example: If a customer changes their email, the old one is replaced.
SCD Type 2 - Historical Tracking
- Each change creates a new record, often with start/end timestamps or versioning.
- Preserves full history of changes.
- Example: Tracking changes to a customer’s loyalty tier over time.
SCD Type 3 – Previous Value
- Stores only the previous value alongside the current one.
- Limited history, useful when only one change needs to be tracked.
- Example: Keeping a “current region” and a “previous region” field for a customer.
Modeling SCDs in Honeydew
In Honeydew, the modeling of SCDs of Types 0, 1, and 3 is straightforward. Joins between entities are defined using the standard foreign key relationships. For SCD Type 2, Honeydew supports modeling SCDs using a combination of foreign keys and date ranges. Joins between entities are defined using a custom SQL expression that includes the date range logic.Example: Fact and a Slowly Changing Dimension (SCD2)
Given two tables:fact_sales: a fact table tracking order transactions that has a foreign keycustomer_iddim_customer: a dimension table tracking customer information over time (SCD Type 2). It has multiple entries percustomer_idwith validity ranges.
Sample data
fact_sales (Fact Table)
dim_customer (SCD2 Dimension Table)
The key for
dim_customer is not customer_id (which is repeating across ranges), but rather a surrogate key (customer_sk)
valid_from / valid_to define the row’s effective period.Also note that valid_to here is an infinity date (9999-12-31). In some settings it is used as NULL instead, in which case can
adjust the join condition accordingly.Relations
To associate each order with the correct customer version at that point in time, use a custom SQL expression onvalid_from and valid_to:
fact_sales to dim_customer
Example query
Result of a query on both:Advanced: Multiple SCD2 (Fact and Dimension) + Point-in-Time Reference point
Advanced use cases for slowly changing dimensions allow to inspect the state of the world at any point in time (including “now”), while every data table has slowly changing dimension fields. Here, the previous example is extended to support of consistent point-in-time queries on historical data where:fact_sales: a fact table with changing business logic over time (e.g. updated amount, revised status). It has multiple versions perorder_id, each valid over a time range.dim_customer: a dimension table with customer history over time (e.g. changed region), also with validity ranges.
dim_date or dim_point_in_time table is used to filter everything as of a specific point.
This structure is used in auditable data models, financial snapshots, and analytics platforms.
Sample data
fact_sales sample data:
dim_customer sample data:
dim_point_in_time: A joint reference point for all data
This is used to filter time centrally, so other joins respect that single reference point. This table can cover all possible dates.
Relations
Fact to customers To associate each order with the corresponding version of the customer that was valid at the time the order version was valid, use the following relation:- Join on customer key and validity ranges
- Direction: Many to one (from
dim_customerto point in time) - Cross-filtering is as needed (one-to-many or bi-directional)
If a customer has multiple versions within the validity time of an order,
it will not be resolved (i.e. will be resolved to NULL).
-
fact_salestodim_point_in_time: -
Many to one (from
fact_salesto point in time) -
Cross-filtering is one-to-many (
dim_point_in_timecan filter the fact, but not vice versa)
-
dim_customertodim_point_in_time: -
Many to one (from
dim_customerto point in time) -
Cross-filtering is one-to-many (
dim_point_in_timecan filterdim_customer, but not vice versa)
The
dim_point_in_time is a shared dimension that can filter all associated entities.Using cross-filtering one-to-many ensures that it will filter the entities, but will not be filtered by them.Ensuring Filtering for a Point in Time
When using SCD with multiple versions, data is duplicated for each snapshot. The semantic modeler must ensure that only one snapshot is selected to prevent double-counting. To ensure consistency at the semantic layer:- Create a copy of
dim_point_in_timecalleddim_point_in_time_choice. That would be used by the user to choose snapshots. - Create a domain that enforces a snapshot choice:
Example query
Status of all orders given reference point of2021-06-15
Status of all orders given reference point of
- Only one valid version per
order_idandcustomer_idis active per point-in-time- Any rows not yet valid are excluded (e.g. 5002 is not visible on 2021-06-15)
2022-05-01