Skip to main content

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.
SCD0 and SCD3 are rarely used.

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 key customer_id
  • dim_customer: a dimension table tracking customer information over time (SCD Type 2). It has multiple entries per customer_id with validity ranges.
Would want to connect the sales data with the correct historical customer information as it was at the time of the order.

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 on valid_from and valid_to:
And set a “many-to-one” relationship from 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 per order_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.
A central dim_date or dim_point_in_time table is used to filter everything as of a specific point.
Since data is duplicated in multiple versions, users must filter on dim_point_in_time to get correct results (whether for “today” or for any historical point of reference).See Conditional Filtering on how to set an automatic filter in a domain, and an example below.You can also ensure a filter is always applied by configuring it directly within user-facing tools, such as BI dashboards.
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.
Same approach can be extended for any type of data versioning - not only for point in time.

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:
  1. Join on customer key and validity ranges
  2. Direction: Many to one (from dim_customer to point in time)
  3. Cross-filtering is as needed (one-to-many or bi-directional)
Relation:
The result of that relation is that customers are resolved to the right appropriate customer to the time of the order, while keeping multiple versions of the order.
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).
Entities to point in time reference
  1. fact_sales to dim_point_in_time:
  2. Many to one (from fact_sales to point in time)
  3. Cross-filtering is one-to-many (dim_point_in_time can filter the fact, but not vice versa)
Relation:
  1. dim_customer to dim_point_in_time:
  2. Many to one (from dim_customer to point in time)
  3. Cross-filtering is one-to-many (dim_point_in_time can filter dim_customer, but not vice versa)
Relation:
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.
Automatic filtering can be done at the dashboard or BI report level. However, a semantic layer allows to enforce automatic filtering across all tools using the same semantics.
To ensure consistency at the semantic layer:
  1. Create a copy of dim_point_in_time called dim_point_in_time_choice. That would be used by the user to choose snapshots.
  2. Create a domain that enforces a snapshot choice:
The dim_point_in_time_choice entity can be used in a BI tool as a filter so a user can pick a snapshot from it.When the BI tool sends the filtered value it is applied to dim_point_in_time by the domain source filter.If the user did not filter for a snapshot, then no data would be returned, thanks to the source filter.The reason dim_point_in_time_choice is created as a copy is to preserve all possible values unfiltered and to allow BI tools to list the possible values. If a BI tool would try listing the values of dim_point_in_time, it would only receive that single chosen snapshot.

Example query

Status of all orders given reference point of 2021-06-15
  • Only one valid version per order_id and customer_id is active per point-in-time
  • Any rows not yet valid are excluded (e.g. 5002 is not visible on 2021-06-15)
Status of all orders given reference point of 2022-05-01
  • Use dim_point_in_time to anchor the reference date
  • Join facts and dimensions based on SCD2 validity ranges
  • Works seamlessly for time travel, reproducible snapshots, or data backfills