skip to content

Late-Arriving Dimensions & Facts

Real pipelines deliver a fact before its dimension row exists, or a dimension change long after the facts it should apply to. Handling both without dropping rows or corrupting history is a strong senior-level signal.

on this pageshow

questions

6

An early-arriving fact references a customer key absent from the customer dimension — what are your options?

level: middleimportance: must knowfreq 55%

answer

  1. the fact still has to land somewhere
  2. dropping the row loses money
  3. one reserved row, or one row per key
  4. keep the natural key so you can repair it
  5. placeholder attributes now, real ones later

basics

~20 s

Never drop the row. Either route the fact to the dimension's reserved unknown member, or — the usual choice — insert an inferred placeholder dimension row carrying the natural key, and enrich it when the real record arrives.

solid answer

~50 s

Three options exist and only two are acceptable. **Dropping the fact** loses revenue and is never right. **Routing to the unknown member** — a single reserved dimension row whose attributes read "Unknown" — keeps the fact countable and the star join inner, but it throws away the arriving natural key, so that row can never be repaired. **Inserting an inferred member** is the standard answer: create a dimension row now, populated with the natural key from the fact and obvious placeholder attributes, flag it as inferred, and let the fact join to its surrogate key. When the source system finally delivers the real record, matched on the natural key, you overwrite the placeholders in place; the surrogate key never moves, so every fact already pointing at it becomes correct with no restatement. Reserve the unknown member for facts that carry no usable natural key at all.

code

sql · 11 lines
sql
-- One inferred member per distinct unmatched natural key, not per fact row
-- customer_sk is generated by the dimension's own key strategy
INSERT INTO dim_customer (
    customer_id, customer_name, segment,
    is_inferred, effective_from, effective_to, is_current)
SELECT DISTINCT
    s.customer_id, 'Unknown', 'Unknown',
    TRUE, DATE '1900-01-01', DATE '9999-12-31', TRUE
FROM   stg_orders s
WHERE  NOT EXISTS (
    SELECT 1 FROM dim_customer d WHERE d.customer_id = s.customer_id);

go deeper

for a junior

Know that the fact must not be dropped and its dimension foreign key must not be left null. Be able to say a placeholder dimension row can be created so the fact has something valid to join to.

for a middle

Explain the difference between the single reserved unknown member and a per-key inferred member, and why carrying the arriving natural key is exactly what makes the second one repairable later.

for a senior

Expect to describe the operational side: how many inferred members exist, how old they are, and what happens when a source feed never delivers the master record at all.

for a principal

Own the policy. Decide how long unresolved placeholders are tolerated, who is accountable for the upstream feed, and how the same handling is applied consistently across every mart instead of reinvented per pipeline.

## What "early arriving" means In every real pipeline the fact feed and the dimension feed are independent. Orders stream in every few minutes; the customer master is a nightly extract. A customer created at 09:00 places an order at 09:05, and the order reaches the warehouse hours before the dimension row does. This is the **early-arriving fact** — the fact is early relative to its dimension, which is equivalently described as a *late-arriving dimension*. It is a timing race between two systems, not a data-quality failure, and no amount of load ordering removes it permanently, because the two sources are not transactionally coupled. ## Why the obvious answers fail **Drop the fact.** The money vanishes. Yesterday's revenue silently understates and nobody knows by how much. This is the answer that ends interviews. **Set the dimension foreign key to NULL.** Warehouses generally do not enforce referential integrity, so nothing stops you — which is exactly the problem. Every inner star join then silently discards the fact, and every outer join forces all downstream SQL to handle NULLs. The star schema's contract is that every fact foreign key resolves to exactly one dimension row. **Hold the whole fact load until the dimension catches up.** The wait is unbounded. One unmatched key blocks an entire feed, and the fact is invisible to reporting for as long as the block lasts. **Park the row in a suspense/reject table.** Legitimate for genuinely unusable data, and every warehouse should have one, but the row stays invisible until a human works the queue. That is the right home for garbage, not for a valid key that simply arrived first. ## Option A — the unknown member A reserved dimension row, present from the day the table is created, whose descriptive attributes all read "Unknown". The fact points at it, the join stays inner, totals stay correct, and reports show an "Unknown" bucket that is itself a useful data-quality signal. The limitation is decisive: nothing on that row identifies *which* customer the fact belonged to. The arriving natural key has been discarded, so the fact can never be repaired without reprocessing the source. Use the unknown member when there is genuinely nothing to repair with — a null source id, an unparseable value, or a dimension role that does not apply to this fact at all. ## Option B — the inferred member Insert a real dimension row immediately: ```sql INSERT INTO dim_customer (customer_id, customer_name, segment, is_inferred) SELECT DISTINCT s.customer_id, 'Unknown', 'Unknown', TRUE FROM stg_orders s WHERE NOT EXISTS (SELECT 1 FROM dim_customer d WHERE d.customer_id = s.customer_id); ``` The row carries the durable business key from the fact feed, obviously-placeholder values for every descriptive attribute, and a flag marking it inferred. The fact resolves to a genuine surrogate key. When the master record finally lands and matches on `customer_id`, the placeholder attributes are overwritten in place. The surrogate key is untouched, so every fact loaded during the gap is instantly correct — no fact restatement, no reload. ## What the inferred row must carry - **The natural key.** This is the entire point; without it the row is just a more expensive unknown member. - **Non-null placeholder attributes**, consistently worded across the whole warehouse. Nulls in dimension attributes break `GROUP BY` and filter predicates in surprising ways; a literal `'Unknown'` groups and filters cleanly. - **An inferred marker** — a boolean or an audit column — so the population is countable and the resolution step is targetable. - If the dimension keeps Type 2 history, an effective window that starts at a low sentinel date, so the fact's own event date falls inside it rather than outside every version. ## Where teams get it wrong Creating one inferred row **per fact row** instead of one per distinct natural key duplicates members and causes fan-out on later joins — deduplicate the staging keys first. Leaving placeholders null pushes the problem into every downstream query. And most commonly, building the insert and never building the resolution, so the warehouse quietly fills with permanent "Unknown" customers. ## Operating it Because the inferred flag exists, monitoring is trivial: count inferred members daily and age them. Transient placeholders that resolve within one load cycle are normal and need no attention. Placeholders older than a few days mean something else — the master record was deleted upstream, the feed is broken, or the fact's key is genuinely invalid — and belong in a data-quality queue with an owner. Some organisations set a ceiling above which the load fails loudly rather than accumulating junk.

  • How do you stop inferred members from silently accumulating forever?
    Track them. The inferred flag makes a daily count trivial, and an aging report on rows inferred more than a few days ago goes to a data-quality queue with a named owner. Persistent placeholders usually mean a deleted master record or a broken feed rather than a timing race, and someone has to decide whether to chase the source or accept the placeholder permanently.
  • Why not just delay the fact load until the dimension row appears?
    Because you cannot bound the wait, and one unmatched key blocks the whole feed. A held fact is invisible to every report until released, so yesterday's revenue understates by an unknown amount. The inferred member gets the money onto the report today and repairs the attributes later, which is strictly better than being silently wrong.
  • What if the fact's source id is null or obviously garbage?
    That is the unknown member's job. There is no natural key to repair with later, so creating a per-row placeholder just breeds junk dimension rows that will never resolve. Point the fact at the single reserved unknown row, keep the raw source value in staging or a reject log for investigation, and count how often it happens.

saying these in an interview costs you the question

  • Drop the fact row until the dimension catches up
  • Set the dimension foreign key to NULL
  • Create a new inferred row for every arriving fact row
  • Use the unknown member even when a valid natural key exists
  • Claim that loading dimensions first makes this impossible

context

open as a page

A late-arriving fact is loaded against the dimension's is_current row — what does that break?

level: seniorimportance: must knowfreq 50%

basics

~20 s

It attributes the event to the wrong dimension version. A six-week-old order keyed to today's current customer row lands in the customer's new segment or territory, so historical reports shift and the Type 2 history you paid to maintain is bypassed entirely.

open as a page

When the real record for an inferred dimension member arrives, why overwrite it instead of adding a Type 2 version?

level: middleimportance: should knowfreq 38%

basics

~20 s

Because the placeholder attributes were never true. A Type 2 version would record a fake history in which the customer really was named "Unknown". Overwrite in place, keeping the surrogate key so existing facts stay attached.

open as a page

How do you retro-fit a late-arriving dimension change into an SCD Type 2 chain that already has newer versions?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Splice 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.

open as a page

How far back should late-arriving data be allowed to restate a warehouse's published history?

level: principalimportance: should knowfreq 28%

basics

~20 s

Set an explicit lateness window per subject area, driven by each source's observed arrival lag and by which periods the business has closed. Inside the window, restate; outside it, book corrections forward and never silently change a signed-off number.

open as a page

After retro-fitting a missed Type 2 dimension version, which existing fact rows must be repointed?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Only facts whose event date falls inside the newly inserted version's window and that still carry the surrogate key of the version that was shortened. Repoint exactly those; facts outside the window are untouched.

open as a page