How do you generate surrogate keys for a warehouse dimension when the platform has no reliable sequence?
answer
- what happens to keys after a full rebuild?
- three families: counter, digest, random
- derived keys need no central counter
- hash the normalized business key
basics
~20 sThree families exist: an identity or sequence counter, a deterministic hash over the business key, or a random UUID. Hashing is the usual warehouse choice because it is reproducible, computable in parallel, and survives a full rebuild without renumbering.
solid answer
~50 sA warehouse dimension needs an identifier the model owns, because the source business key can be re-issued and because a history-keeping dimension holds several rows per entity. Three strategies compete. **Identity/sequence** gives narrow 4-8 byte keys, but the value is *assigned*, not derived: rebuild the dimension and every fact already loaded still points at the old numbers. Most distributed warehouses also promise only uniqueness, not gap-free ordering. **Deterministic hash** — a digest over the normalized business key, plus the version identifier for a Type 2 dimension — is reproducible on any machine in any order, so rebuilds are safe and the fact loader can compute the key rather than joining back to the dimension; the cost is 16-32 wide bytes in your largest tables and opaque values. **Random UUID** is unique but non-deterministic, so it inherits the rebuild problem at hash width. Whichever you pick, keep the business-key columns in the dimension row.
go deeper
Know that fact tables join to dimensions on a key the warehouse itself mints, not on the source system's identifier, and that the source identifier is still stored as an ordinary column.
Be ready to compare a counter, a deterministic digest and a random identifier, and to explain what each does when the dimension is fully rebuilt while facts are not.
Show that you have lived through a renumbering incident: what breaks, how you detect it, and which rule you put in place afterwards about rebuilding a dimension independently of its facts.
Own the key recipe as a platform contract — width versus reproducibility across a fact estate, cross-environment and cross-system key alignment, and the migration cost of ever changing the scheme.
## What a warehouse surrogate key is for In a dimensional model each dimension row carries an identifier the *model* owns rather than the source system: the surrogate key. Fact rows store that key instead of the source's business key. Two warehouse-specific reasons drive this. First, the source identifier is outside your control — it can be re-issued after a system migration, reformatted, or merged when two customer records are deduplicated. Second, a dimension that keeps history has more than one row per business entity, so the business key can no longer identify a row on its own. The question is not *whether* to mint a key but *how* the value is produced. ## Option 1 — a sequence or identity column A counter is the cheapest key you can have: 4 or 8 bytes, narrow joins, and values that roughly follow load order. The catch in a warehouse is that the value is *assigned at load time*, not derived from the data. Consequences: - **Rebuilds renumber.** A full refresh, a re-run after a bad load, or a migration to another platform gives the same customer a different number, while fact rows already loaded still carry the old one. You must either never rebuild the dimension alone, rebuild dependent facts in the same operation, or persist a key map. - **Assignment serializes.** The fact loader has to look the key up in the dimension, adding a join to every load. - **Ordering is not a contract.** Distributed warehouses hand each loading slice a block of values, so identity columns are typically unique but not gap-free and not ordered. Never sort or range-filter on a surrogate key. ## Option 2 — a deterministic hash Compute the key as a digest over the business key, normalized, and — for a dimension that versions rows — over whatever identifies the version. Same input, same key, forever, on any machine, in any order. That buys three things: rebuilds are safe because the key is derived rather than assigned; the build parallelizes with no central counter; and the fact loader can *compute* a dimension key from columns it already holds instead of joining back. The costs are real. The key is 16-32 bytes (worse if stored as a hex string), and it lands in the fact table, which is the biggest object you own. Keys are opaque, so debugging means joining back to see who a key belongs to. And the key is only as stable as the normalization recipe — change trimming or case rules and every key in the warehouse changes at once. Also note that for a history-keeping dimension the hash must cover the version, so a loader that knows only the business key still needs a point-in-time lookup to decide *which* version a fact belongs to. ```sql -- key derived from data, not assigned by a counter SELECT digest(UPPER(TRIM(customer_id)) || '|' || CAST(effective_from AS VARCHAR)) AS customer_sk FROM stg_customer; ``` ## Option 3 — a random UUID Generated without coordination and available everywhere, but derived from nothing. It shares the sequence's rebuild problem — a re-run mints new values — while being as wide as a hash. In a warehouse this is usually the worst of both, and it mostly appears when the identifier was minted upstream and simply carried in. That case is fine: it is then a *business* key that happens to look like a UUID, and you still decide separately how to key the dimension. ## Choosing - **Single loader, rare full rebuild, storage-sensitive fact tables:** a sequence or identity column, plus a written rule that the dimension is never rebuilt without rebuilding dependent facts. - **Frequent full refreshes, parallel or multi-environment builds, keys that must line up between dev and prod or across systems:** a deterministic hash. - **Anything else:** default to the hash. Reproducibility is worth more than eight bytes per fact row in most shops, and the moment someone re-runs a model you find out which choice you made. ## Rules that hold either way Keep the business-key columns physically in the dimension row: the surrogate is for joining, the business key is for reconciling against the source and for a human reading a row. Reserve fixed key values for the unknown and not-applicable members so facts never carry a NULL dimension reference. Publish the key recipe as a contract — anything downstream that stores a key depends on it. And never expose a surrogate value in a report or a URL: it is an internal join token that can legitimately change when the model is rebuilt.
- Why is it a bad idea to sort or range-filter fact rows on a surrogate key?Because the key is a join token, not a measurement. Distributed warehouses hand each loading slice a block of identity values, so the sequence is unique but neither gap-free nor ordered by load time; hash and UUID keys have no order at all. If you want load order, store an explicit load timestamp or batch id column and filter on that.
- If a dimension uses hash keys, do you still need the business key columns in the table?Yes. The hash is one-way, so without the business key columns nobody can tell which entity a row describes, reconcile a total against the source system, or debug an unexpected join result. Keep the raw business key and its source system name as ordinary attributes; the surrogate exists only so facts have something stable to join on.
- What breaks if you change the normalization rules inside a hash key recipe?Every key derived from it changes, so the next run mints a full set of new dimension rows and existing fact rows point at values that no longer exist. Treat the recipe — the trim, the case fold, the NULL sentinel, the delimiter, the column order — as a published contract, versioned and changed only with a coordinated rebuild of the dimension and its facts.
A sequence key is like a cloakroom ticket number — meaningful only for this evening. A hash key is like a fingerprint: recompute it any time and you get the same answer.
saying these in an interview costs you the question
- Says surrogate keys exist for index locality — that is the OLTP answer
- Assumes warehouse identity columns are gap-free and ordered
- Uses a random UUID and expects rebuilds to be reproducible
- Hashes the raw business key without trimming or case folding
- Drops the business key columns once the surrogate exists