How do you retro-fit a late-arriving dimension change into an SCD Type 2 chain that already has newer versions?
answer
- the change is dated in the past
- which version's window contains that date?
- don't touch the newest row's flag
- insert into the middle of the chain
- the facts in that window are now mis-keyed
basics
~20 sSplice it into the middle of the chain, not onto the end. End-date the existing version whose window covers the change date, insert the missed version starting at that date and ending where the next version begins, and leave the newest row's current flag untouched.
solid answer
~50 sA normal Type 2 load assumes changes arrive in order: end-date the current row, insert a new one, mark it current. A change dated three months ago breaks that assumption, and running the normal path produces the classic bug — the true current attributes get end-dated and a stale value becomes the current row. Instead, locate the version whose effective window **contains the change date**, shorten it to end the day before, and insert the missed version with `effective_from` at the change date and `effective_to` at the day before the next version starts. The inserted row is not current unless the change date is later than every existing version. Then deal with the consequence: fact rows whose event dates fall in the newly carved window still point at the version that was just shortened, so they are attributed to the wrong dimension values until repointed.
code
text · 15 linesdim_customer, natural key 88123 -- BEFORE (inclusive end dates)
sk effective_from effective_to city is_current
9041 1900-01-01 2024-03-31 Berlin false
9502 2024-04-01 9999-12-31 Hamburg true
A change dated 2024-02-10 (Berlin -> Bremen) arrives on 2024-05-20
WRONG - ordinary load path, appended at the end
9502 2024-04-01 2024-05-19 Hamburg false
9611 2024-05-20 9999-12-31 Bremen true
RIGHT - spliced into the covering window
9041 1900-01-01 2024-02-09 Berlin false
9611 2024-02-10 2024-03-31 Bremen false
9502 2024-04-01 9999-12-31 Hamburg truego deeper
Recall that a Type 2 dimension keeps one row per version with an effective window, and that a change dated in the past cannot simply be appended after the newest row.
Explain the splice: locate the version whose window contains the change date, shorten it, and insert the missed version bounded by the next version's start rather than the high sentinel.
Demonstrate that you would guard the result — overlap and single-current-row checks after every load — and that you know the facts inside the carved window are now mis-keyed and need repointing.
Decide how far back retro-fits are permitted at all, who signs off when a published period's numbers move, and whether sources without reliable change dates get this machinery or a documented accuracy limit.
## Why the ordinary load path fails A Type 2 dimension load is normally written for a monotonic world. It finds the row flagged current, sets its end date to the day before the change, inserts a new row from the change date to the high sentinel, and marks that row current. Every step of that is wrong when the change is dated in the past. Suppose customer 88123 has two versions — Berlin until 31 March, Hamburg from 1 April, current. On 20 May a change dated **10 February** arrives: Berlin to Bremen. Run the ordinary path and you get: Hamburg end-dated 19 May, Bremen current from 20 May. The customer's genuinely current city has been retired in favour of a value that was superseded three months ago, and the February-to-March window still says Berlin. Two errors from one load. This is why late-arriving dimension handling is treated as a separate code path rather than a variation on the normal one, and why interviewers ask about it — it is a bug that a naive implementation ships and nobody notices until a report is questioned. ## The splice The correct operation is an insertion into the middle of an ordered chain. Three steps: **1. Find the covering version.** Not the current row — the row whose effective window contains the change date: ```sql SELECT customer_sk, effective_from, effective_to FROM dim_customer WHERE customer_id = 88123 AND DATE '2024-02-10' BETWEEN effective_from AND effective_to; ``` **2. Shorten it.** Set its `effective_to` to the day before the change date (or to the change date itself, if your convention uses exclusive end bounds — pick one convention and apply it everywhere). **3. Insert the missed version.** `effective_from` is the change date. `effective_to` is derived from where the *next* version begins, not hardcoded and not the high sentinel. The `is_current` flag is FALSE, because a later version exists. The result is a chain that is still contiguous, still gap-free, still has exactly one current row, and now has three versions instead of two. ## What must not change **The newest row's current flag.** Only one row per business key may be current, and it is the one with the latest `effective_from`. A retro-fit in the past never touches it. **Existing surrogate keys.** The shortened row keeps its key. Reissuing keys would orphan facts. **The chain's contiguity.** After the splice, every date between the earliest `effective_from` and the high sentinel must fall inside exactly one version. A gap means facts land nowhere; an overlap means a fact matches two versions and fans out, silently multiplying measures. ## Verify the chain, always Retro-fits are exactly the operation that produces overlaps, so make the check routine rather than occasional: ```sql SELECT a.customer_id FROM dim_customer a JOIN dim_customer b ON a.customer_id = b.customer_id AND a.customer_sk <> b.customer_sk AND a.effective_from <= b.effective_to AND b.effective_from <= a.effective_to; ``` Any row returned is an overlapping pair. A second check counts current rows per business key and expects exactly one. ## The fact-side consequence Carving a new window out of an existing one means facts that fall inside it are now attributed to a version that no longer covers their event date. Those rows still carry the shortened version's surrogate key and will report Berlin for a period when the customer was in Bremen. The dimension-side splice is only half the job; the fact rows in the carved window have to be repointed at the newly inserted surrogate key, and anything derived from them for that period has to be rebuilt. ## Two cases that need a decision, not a formula **The change predates every existing version.** There is no covering row. Either back-date the earliest version's `effective_from` to a sentinel and insert the new version ahead of it, or accept that the change is older than the dimension's known history and log it. Decide deliberately; do not let the load silently do nothing. **Several late changes arrive together, out of order among themselves.** Do not apply them one at a time through the same code path. Sort all pending changes for the business key by effective date, rebuild the chain for that key as an ordered sequence, and write it once. Applying out-of-order changes sequentially through a splice routine is how overlaps get created. ## Where the change itself comes from Detecting that a source row *is* a late change — comparing arriving attributes against the version that was in force on the source's own change timestamp — is the load's job, and it depends on the source actually supplying a reliable change date. When it does not, and all you have is a load timestamp, retro-fitting is guesswork: you cannot place a version in a chain without knowing when it started. That limitation is worth naming in an interview, because it is the reason many teams accept slightly-wrong history rather than build the machinery.
- How do you verify the chain is still valid after a retro-fit?Two checks, run every load. First, a self-join on the business key looking for any pair of versions whose effective windows overlap — a retro-fit that shortens the wrong row produces exactly that. Second, a count of current rows per business key expecting exactly one. Add a gap check if your convention requires contiguity, since a gap makes facts in the hole match no version at all.
- What if several late changes for the same business key arrive in one batch, out of order?Do not push them through the splice path one at a time — that is how overlaps appear. Sort all pending changes for the key by effective date, rebuild that key's whole version chain as an ordered sequence in memory or a staging set, and write it once. Rebuilding one key's chain is cheap; repairing a corrupted one is not.
- What do you do when the late change predates every version in the dimension?There is no covering row to shorten, so the load must not silently do nothing. Either back-date the earliest version's effective_from to a low sentinel and insert the new version ahead of it, or declare the change older than the dimension's known history and log it for review. The point is that it is an explicit decision, not a fall-through.
saying these in an interview costs you the question
- End-date the current row and mark the late change current
- Append the missed version at the end of the chain
- Set the inserted version's effective_to to the high sentinel
- Assume source changes always arrive in effective-date order
- Fix the dimension and ignore the facts in the carved window