Data Engineering glossary
What is a Surrogate Key?
A surrogate key is an identifier generated by the warehouse, usually an integer or a hash, that has no business meaning and exists only to join facts to dimensions. It lets one entity have several historical versions, combines records from sources that reuse the same ids, and survives source system re-keying.
How it works
A natural key (business key) is the id the source uses: a customer number, an SKU, an email address. A surrogate key is created by the warehouse. Dimensions need surrogates for several reasons:
- History. Under SCD Type 2, a customer who moved city twice has three rows that share the natural key; each version needs its own key, and facts point at the version that was current when they happened.
- Several sources. The CRM and the billing system may both have a customer
1001who are different people; the natural key becomes the pair (source system, source id). - Source changes. Systems get migrated and re-keyed, ids get recycled. With surrogates, only the mapping changes; facts do not.
- Special members. Unknown and not-applicable rows need keys (often -1 and -2) so facts never carry NULL foreign keys.
- Smaller fact rows: a 4- or 8-byte integer instead of a long string, although columnar compression narrows this gap.
The natural key still stays in the dimension: loads use it to find the existing row, and analysts search by it.
Surrogates are generated either by a sequence (small, needs one central generator) or by hashing the source system and natural key (larger, but any parallel job can compute the same value without a lookup). Classic Kimball warehouses use integers; Data Vault and many lakehouse pipelines use hash keys.
Example
Two versions of one customer share the natural key but get different surrogate keys from an identity column (PostgreSQL 16).
CREATE TABLE dim_customer (
customer_key int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id text NOT NULL,
city text,
valid_from date,
is_current boolean
);
INSERT INTO dim_customer (customer_id, city, valid_from, is_current) VALUES
('C1', 'Pune', '2025-01-01', false),
('C1', 'Mumbai', '2026-03-10', true),
('C2', 'Leeds', '2025-06-01', true);
SELECT customer_key, customer_id, city, is_current FROM dim_customer ORDER BY customer_key;
customer_key | customer_id | city | is_current
--------------+-------------+--------+------------
1 | C1 | Pune | f
2 | C1 | Mumbai | t
3 | C2 | Leeds | t
Common mistakes
- Using the source id as the dimension’s primary key, which makes Type 2 history impossible.
- Generating surrogates in each pipeline run with a non-deterministic method, so a rebuild changes every key and breaks facts.
- Hashing the natural key alone for a Type 2 dimension, which gives every version the same key; include the version start or keep a separate version key.
- Dropping the natural key from the dimension, so loads cannot find existing rows.

