Slowly changing dimensions are a tricky problem in data engineering that every data team will grapple with at one point or another. Traditional approaches require a bit of data engineering jiu-jitsu to model them accurately. But the Activity Schema approach allows for an elegant solution that is easy to implement while maintaining a high degree of accuracy. This blog explains how it's done.
Slowly changing dimensional data looks just like regular dimensional data ("dim tables"), but the values are slowly changing over time. Slowly changing dimensions (SCDs) are common when representing attributes about a user, like company size. Most companies will stay within the same size bracket year over year, but some companies will grow or downsize. This is a slowly changing dimension because the jump from the 1-20 to 20-100 bracket is an infrequent and barely noticeable change, but it happens slowly over time.
This example may seem obvious, but in practice there are many non-obvious dimensions that behave as SCDs, like contracts, policies, location, etc. Anil Vaitla shares a number of helpful examples in his blog post for software engineers to help you spot slowly changing dimensional data in the wild.
Slowly changing dimensions are a sneaky problem because it may not seem like an important issue to solve for today. Who will notice if a customer changes their address every 2 years? or ...if a user updates their content preferences in the app? But as time goes on, this seemingly harmless edge case can cause big problems for data teams if not handled correctly at the onset.
If not handled correctly, the data in your models will start to diverge from the source of truth. And, even when SCD's are addressed, there is the added challenge of ensuring that historical values are maintained for the data analysts and data scientists to use if needed. Unfortunately most of the standard approaches (types 1 - 7) for handling SCDs are still complicated for a data user to use correctly and tedious for a data engineer to instrument.
This quote from Tom Johnston's article sums it up well...
In brief, there are two things wrong with SCDs. The first is that they inadequately live up to their stated purpose. They are intended to provide history for dimensions; but the history they provide is incomplete. This objection applies to all SCD types, used alone or in combination.
The second is that the costs of using “advanced SCDs” (SCD types 3 – 7) outweigh any cost savings that those advanced SCDs achieve over type 2 SCDs.
No matter how you approach this pesky data, you're likely to run into challenges of some nature.
There are numerous methods of modeling SCD's with a traditional star schema. I won't go into all of them here, but I will talk about two popular approaches and their downfalls.
This approach maintains a historical record of changing dimensions by taking a "snapshot" of data and appending it to a history table. The data is snapshotted on a regular basis (usually daily) so that we always have a picture of what the world looked like at a given time.
The nice thing about these tables is that they're easy to use. Your data analyst or data scientist will likely use this table often, but ultimately you'll run into some limitations:
valid_from
and valid_to
timestampsWith this approach, you'll add a new record any time the data changes and maintain a valid_from
and valid_to
timestamp for that value. This will accurately capture the changes over time and give the data user enough detail to understand exactly when the data changed.
Unfortunately, these tables are hard to use. Most likely these tables would not be widely adopted - or worse, they would be adopted but used incorrectly - because they require complex conditional joins on the timestamp column. 😰
Data scientists and data analysts do not have the SQL skills of a data engineer, and that's to be expected. Their core skills are data storytelling, visualization, analysis, and statistics; SQL is just a means to an end.
Ultimately, if a table requires conditional joins to use it accurately, we're setting ourselves (and our team) up for failure.
The activity schema approach to SCDs overcomes the core challenges we ran into with a star schema.
Benefits
Whenever a dimension changes, that update is modeled as its own activity in the activity schema to represent when the change occurred and the new value.
Let's consider an example: Account Size
Over time, a customer may change the size of their account. This won't happen often, but when it does the change it will be important to track. In an activity schema, an activity is added to the activity stream each time the account size is updated.
In the journey below, you'll see an Updated Account Size activity each time the account size is updated. There are three activities in this customer's journey because the account size has been changed three times: from 4,800 to 1,300 to 2,500.
Activities are flexible enough to capture repeated changes over time, so they are perfect for slowly changing dimensions - or frankly, any changing dimensions.
In an activity schema, we're forced to define independent concepts (activities) that are immutable, which solves the problem of SCDs completely. Compare that with the star schema approach, where most SCDs are combined with other tables to make them easier to work with – denormalized to make them easier to query – but then it becomes less flexible and much harder to maintain.
Traditional approaches to SCD's can create a lot of unnecessary bloat in the warehouse or require a lot of processing power to scan the entire table for changes on each processing run. The Activity Schema approach takes advantage of the incremental data processing when adding new activities.
Let's look at the logic for Updated Account Size...
Note that the timestamp of the Updated Account Size activity is the updated_at
timestamp from the account_details
table. And, recall that in an activity schema, new activities are added to the activity stream incrementally by default. That means, on each processing run, it checks to see if there are any updated_at
timestamps that happen after the last updated_at
from the previous run and append any new data to the activity stream. In the case of SCDs, this means that each time a dimension is updated a new updated_at
timestamp is assigned and the new value is added to the activity stream to create the history of changes that we were looking at in the customer journey above.
updated_at
timestamps after the last runWith the incremental processing of the activity stream, slowly changing dimensions are updated seamlessly without any overhead from the data engineering team.
We've discussed the processing benefits of modeling SCDs with an activity schema, but that's only meaningful if the data end-user can easily use the modeled data. Let's see what that looks like with an activity schema...
The activity schema uses relational operators ("relationships") to assemble activities together into a dataset for modeling or analysis. These operators are the secret to providing flexibility to the data user, without a single conditional join. If you're unfamiliar with the way that datasets are assembled with an activity schema, you can read more about it in this spec.
The activity schema gives data users the flexibility to incorporate SCDs into any dataset without the complexity of conditional joins.
Let's walk through an example of how a data user might incorporate Account Size into their dataset for three different scenarios.
We'll start with a dataset of support requests, based on the Requested Support activity, so each record of our dataset is a single a request that has been submitted by a customer. In an activity schema, it's defined this way:
Consider three ways a data user may want to use the Account Size dimension in the support dataset:
All three scenarios are simple to create using an activity schema.
Scenario 1: All support requests with the initial size of each account...
This dataset uses the FIRST EVER relationship to build a dataset with the initial size of the account.
Scenario 2: All support requests with the current size of each account...
This dataset uses the LAST EVER relationship to build a dataset with the current size of the account.
Scenario 3: All support requests with the account size at the time of the request submission...
This dataset uses the LAST BEFORE relationship to find the last time the account size was updated before each support request was submitted. Since this was the last update before the submission, it represents the account size at the time of the request.
Each of these scenarios are simple with an activity schema, requiring no complex logic but providing the flexibility to bring in the SCD as it's appropriate for each new data question.
Slowly changing dimensions can be a lot of work, but they don't have to be if you're using an Activity Schema.
With this approach, processing is minimal, the data engineering is simple, and the end-data user can incorporate them into any dataset with ease. If you're interested trying the Activity Schema approach for yourself, I recommend checking out Narrator, a data platform that leverages the activity schema to enable you to build datasets and answer questions in minutes. Setup is quick (< 1 day) and you can experience the simplicity of modeling SCDs in an activity schema using your own data.