How do you add a tracked attribute to a live SCD Type 2 dimension without exploding its history?
answer
- what does the stored hash encode implicitly?
- every row suddenly looks changed — why?
- fix it in the same deployment
- what history does the new column have?
- reordering columns is the same breaking change
basics
~20 sRecompute the stored change hash for existing current rows under the new column list in the same deployment. Otherwise every stored hash was built over the old list, every incoming hash differs, and the next run closes and re-versions every member for no business reason.
solid answer
~50 sThe stored hashdiff encodes its column list implicitly, so widening the tracked set makes every stored hash incomparable with the newly computed one and the next load versions the entire dimension on deployment day. The cheap fix is to backfill: in the same release that changes the hash expression, recompute and update the hash on each key's current version using the new definition, so only genuine attribute changes produce versions afterwards. Two other options exist — accept one deliberate re-version wave and label it, or replay the full history from source if the source can supply it — and both are more expensive, the replay especially because it renumbers surrogate keys that facts already reference. Whichever you pick, be honest that the new attribute has no history before today: you can only track it going forward, so decide whether to stamp its current value onto existing versions or leave it NULL, and tell consumers which.
code
sql · 9 lines-- run in the same release as the hash-expression change, before the next load
update dim_customer
set hashdiff = md5(
coalesce(cast(customer_name as varchar(4000)), '~') || '|' ||
coalesce(cast(city as varchar(4000)), '~') || '|' ||
coalesce(cast(tier as varchar(4000)), '~') || '|' ||
coalesce(cast(segment as varchar(4000)), '~') -- newly tracked
)
where is_current = 1go deeper
Know that the change hash on a dimension row was computed from a specific list of columns, so changing that list makes every stored hash look different even when nothing about the member changed.
Be able to predict the symptom — a full-dimension version wave dated on deployment day — and describe the backfill that recomputes stored hashes under the new definition in the same release.
Expect to weigh backfill against a deliberate version wave against a full replay, and to argue why a rebuild is dangerous when facts already reference the existing surrogate keys.
Own the tracked set as a published contract: it defines what history the business pays to keep, its changes are deployment events with consumer impact, and the pipeline should assert on version volume so an accidental wave fails rather than lands.
## Why the change is disruptive at all A Type 2 load decides whether to version a member by comparing a stored hash of the tracked attributes against a freshly computed one. That stored hash is a function of a specific column list, in a specific order, with a specific NULL sentinel and delimiter — but the list itself is not stored anywhere. It lives only in the transformation code. So the moment you add a column to the hash expression, every stored hash in the dimension was computed under the old definition and every incoming hash under the new one. They differ for essentially every row. The next run closes every current version and inserts a replacement, producing a version wave across the entire dimension dated on deployment day, with no business change behind any of it. On a large dimension that is an expensive write, a distorted history, and a pile of confused questions from analysts who see every customer "change" on the same date. The same trap fires for changes that look purely cosmetic: reordering the columns in the expression, switching the delimiter, adding a `trim()`, or changing a cast. Any edit to the hash input is a breaking change to comparability. ## Option 1 — backfill the hash under the new definition The cheapest and usually correct answer. In the same deployment that changes the expression, run a one-time update that recomputes the hash for every current row using the new column list and the values already on that row: ```sql update dim_customer set hashdiff = md5( coalesce(cast(customer_name as varchar(4000)), '~') || '|' || coalesce(cast(city as varchar(4000)), '~') || '|' || coalesce(cast(tier as varchar(4000)), '~') || '|' || coalesce(cast(segment as varchar(4000)), '~') -- newly tracked ) where is_current = 1; ``` After this, the stored and computed hashes agree again for unchanged members, and the next run versions only members whose attributes genuinely differ. Two details matter: run the backfill and the code change atomically enough that no load executes between them, and use the values on the dimension row rather than re-reading the source, so you do not accidentally absorb a real pending change into the backfill and lose its version boundary. ## Option 2 — accept one deliberate version wave If the dimension is small, or if the new attribute's introduction is itself a meaningful event, you can let the wave happen — but only deliberately: announce it, date it at a known boundary, and mark the resulting versions (a load-reason or change-reason column) so nobody mistakes deployment noise for business change. This is rarely worth it on a large dimension and never worth it as an accident. ## Option 3 — replay history from source If the source can supply the new attribute's full change history, you can rebuild the dimension so the new column has real history rather than starting today. This is the only option that produces genuinely correct historical values for the new attribute — and it is the most expensive. A rebuild reassigns surrogate keys, so every fact table, extract and cached report holding those keys must be remapped or reloaded. Reserve it for cases where the historical values of the new attribute actually drive decisions. ## The honest limitation: no history before today Whatever you choose, be explicit that a newly tracked attribute has no past. Existing versions were written without it. You have two defensible treatments and must pick one and publish it: - **Stamp the current value onto all existing versions.** Queries never hit NULL, but the dimension now asserts something false — that the member always had today's value. That is exactly the retroactive restatement Type 2 exists to prevent. - **Leave it NULL on versions that predate the change.** Honest and self-documenting: a NULL means "not tracked then". Consumers must handle it, which is a cost, but the model does not lie. The second is usually right, and the deciding factor is whether downstream consumers can tolerate NULL. ## Governance, which is the real answer at this level The recurring lesson is that the tracked set is a contract, not a detail. Treat it accordingly: keep the column list in code next to the dimension so a reviewer sees it in the diff; require the hash backfill in the same change that edits the expression, ideally as a checklist item in the review; assert after deployment that the number of versions created is in a plausible range, so an accidental full-table wave fails the run instead of landing; and remember that removing a column from the tracked set is the same breaking change in reverse — plus a policy decision that changes to that attribute will no longer be recorded at all.
- Does removing a column from the tracked set carry the same risk?Yes, mechanically — the hash expression changes, stored hashes become incomparable, and the next run re-versions everything unless you backfill. It also carries a policy consequence the addition does not: future changes to that attribute will no longer create versions, so it silently becomes a Type 1 overwrite. Both effects need announcing, not just the write amplification.
- How do you keep an accidental full-table version wave from landing at all?Assert on the size of the changed set before or right after the write: fail the run when the number of versioned members exceeds a plausible threshold for that dimension, with an explicit override flag for deployments where a wave is intended. It turns a silent, expensive mistake into a failed load somebody looks at.
- Should the new attribute be backfilled onto historical versions with its current value?Usually not. Stamping today's value onto old versions asserts the member always had it, which is the retroactive restatement Type 2 exists to prevent. Leaving it NULL on pre-change versions is honest and self-documenting — NULL means "not tracked then". Choose the stamp only if consumers genuinely cannot handle NULL, and publish the choice either way.
saying these in an interview costs you the question
- Editing the hash expression without backfilling stored hashes
- Assuming a column-order change is cosmetic
- Stamping today's value onto historical versions as if it were history
- Rebuilding the dimension while facts hold its surrogate keys
- Treating the tracked column list as an implementation detail