skip to content

Warehouse Keys & Naming Conventions

Surrogate, hash, and natural keys in a warehouse where sequences are expensive and foreign keys are not enforced, plus the dim_/fct_/stg_ naming discipline that makes a model readable to everyone downstream.

on this pageshow

questions

6

How do you generate surrogate keys for a warehouse dimension when the platform has no reliable sequence?

level: middleimportance: must knowfreq 55%

answer

  1. what happens to keys after a full rebuild?
  2. three families: counter, digest, random
  3. derived keys need no central counter
  4. hash the normalized business key

basics

~20 s

Three 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 s

A 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

Why does a warehouse dimension carry an 'Unknown' member row instead of leaving the fact's key NULL?

level: middleimportance: must knowfreq 45%

basics

~20 s

A NULL dimension key drops the fact row from every inner join to that dimension, silently changing totals. A reserved Unknown member row keeps the fact joinable, so measures still add up and the missing value shows as its own visible bucket.

open as a page

What do the dim_, fct_, stg_ and int_ prefixes on warehouse table names mean?

level: juniorimportance: should knowfreq 48%

basics

~20 s

They mark a table's role in the pipeline: stg_ is a lightly cleaned copy of one source, int_ an intermediate step nobody should query directly, and dim_ and fct_ are the dimension and fact tables of the published model.

open as a page

If a warehouse doesn't enforce foreign keys between fact and dimension tables, why declare them at all?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A declared but unenforced foreign key is metadata: it documents the join path, feeds BI and lineage tools, and can enable optimizer rewrites. It guarantees nothing — orphan fact rows still load — so referential integrity has to be checked by the pipeline.

open as a page

How would you enforce column and table naming standards across a warehouse many teams publish into?

level: principalimportance: should knowfreq 30%

basics

~20 s

Write the standard as a short, mechanical rule set, enforce it with an automated check in the build rather than in code review, apply it strictly to new models, and retrofit existing ones only behind aliasing views with an owned exception register.

open as a page

What can go wrong when a warehouse surrogate key is a hash of the business key columns?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Concatenating raw columns collides when the delimiter is ambiguous, and produces different keys when NULLs, case or whitespace vary between loads. Normalize each part, replace NULL with a sentinel, and join with a delimiter that cannot occur in the data.

open as a page