Chapter 03 · Dimensional Modeling Foundations: Facts, Dimensions, Star Schemas, and Query Semantics

Foreign-Key Relationships, Surrogate Keys, Referential Integrity, and Orphan Handling

Resolve AtlasMart business keys into warehouse surrogate keys, enforce referential integrity, and handle unresolved dimensions without silently losing facts.

Intermediate → Advanced100–120 minutesOrphan + unknown-member labPython 3.13.5 · SQLite 3.46.1 testedLast reviewed: September 2026

Learning outcomes

A star schema is only trustworthy if every fact foreign key resolves to exactly one dimension row. Natural source keys are necessary for matching incoming records, but warehouse joins need durable keys under warehouse control. Missing dimension context must be represented explicitly; dropping the fact or letting a null foreign key break the join corrupts analytical totals.

01

Explain the distinct jobs of natural/business keys, dimension surrogate keys, and fact foreign keys.

02

Enforce referential integrity for the canonical AtlasMart star.

03

Use an explicit Unknown dimension member when source context cannot yet be resolved.

04

Demonstrate how an inner-join load can silently lose a fact row.

05

Prove that the repaired load preserves control totals while making the data-quality exception visible.

Executed baseline for this chapter

The local verification scripts in this generated chapter were executed with Python 3.13.5 and SQLite 3.46.1. They use only stable standard-library/SQL features. Learners should still record the versions printed on their machine; the exercises prove semantic contracts on the synthetic fixture, not performance of a production warehouse.

1. Natural keys identify source entities; surrogate keys identify warehouse rows

C001 and P100 are business/natural keys supplied by source systems. They are valuable for matching incoming records, lineage, and reconciliation. They are not ideal as the fact table's dimensional foreign keys because source keys can be reused, reformatted, collide across systems, or later require multiple historical dimension rows.

The warehouse therefore assigns customer_key, product_key, and store_key. Facts join to those surrogate keys. Later SCD chapters can create a new surrogate row for the same natural key without changing the source identity itself.

2. Referential integrity is a semantic invariant

Every non-null dimensional foreign key in fact_sales must match one dimension row. In this local harness SQLite enforces that rule when PRAGMA foreign_keys=ON. Some analytical engines treat constraints as informational or do not enforce them, so the warehouse pipeline must still test the invariant explicitly.

SQL · orphan checks
SELECT f.sales_key, f.customer_keyFROM fact_sales f LEFT JOIN dim_customer d  ON d.customer_key=f.customer_keyWHERE d.customer_key IS NULL;SELECT f.sales_key, f.product_keyFROM fact_sales f LEFT JOIN dim_product d  ON d.product_key=f.product_keyWHERE d.product_key IS NULL;SELECT f.sales_key, f.store_keyFROM fact_sales f LEFT JOIN dim_store d  ON d.store_key=f.store_keyWHERE d.store_key IS NULL;

All three result sets must be empty for the canonical fixture.

3. Unknown is data, not absence of a key

Sometimes the fact event is legitimate but descriptive context is unavailable at load time. A null foreign key or a dropped fact hides that condition. The lab reserves surrogate key 0 for a visible Unknown member in each non-date dimension. This permits the fact to load while preserving both the measurement and the fact that context resolution failed.

Unknown and Not Applicable are not always the same business state. Production models often use separate default members. This chapter uses one Unknown member to keep the fixture focused; later designs should choose labels and keys with business users and data-quality owners.

4. Hands-on failure injection — prove why inner-join loss is dangerous

Reuse atlasmart_star.sql and run this isolated failure lab. The extra P999 record is deliberately not part of the canonical Chapter 03 data; it exists only to expose orphan behavior.

orphan_lab.py
import sqlite3from pathlib import Pathconn=sqlite3.connect(":memory:")conn.execute("PRAGMA foreign_keys=ON")conn.executescript(Path("atlasmart_star.sql").read_text())# Isolated staged event: valid sale measurement, product reference not yet known.stage={"order_id":"O9999","line_no":1,"date_key":20260920,"customer_id":"C004",       "product_id":"P999","store_id":"S-MOBILE","quantity":1,"unit_price":30}stage["amount"]=stage["quantity"]*stage["unit_price"]# Wrong: inner lookup returns no product row, so a naive INSERT..SELECT would load 0 rows.product=conn.execute("SELECT product_key FROM dim_product WHERE product_id=?",(stage["product_id"],)).fetchone()print("naive product lookup =", product)assert product is None# Repair: map unresolved context to the governed Unknown member and retain the amount.product_key=0customer_key=conn.execute("SELECT customer_key FROM dim_customer WHERE customer_id=?",(stage["customer_id"],)).fetchone()[0]store_key=conn.execute("SELECT store_key FROM dim_store WHERE store_id=?",(stage["store_id"],)).fetchone()[0]conn.execute("""INSERT INTO fact_sales(sales_key,date_key,customer_key,product_key,store_key,order_id,line_no,quantity,unit_price,extended_amount,source_batch_id)VALUES (?,?,?,?,?,?,?,?,?,?,?)""",(999,stage["date_key"],customer_key,product_key,store_key,stage["order_id"],stage["line_no"],stage["quantity"],stage["unit_price"],stage["amount"],"FAILURE-LAB"))canonical=625new_total=conn.execute("SELECT SUM(extended_amount) FROM fact_sales").fetchone()[0]unknown_total=conn.execute("SELECT SUM(extended_amount) FROM fact_sales WHERE product_key=0").fetchone()[0]print("canonical=",canonical,"after staged exception=",new_total,"unknown_product_amount=",unknown_total)assert new_total == 655 and unknown_total == 30

The naive lookup returns None. A load written as an inner join would silently omit the 30-unit-of-currency measurement. The repaired load maps the unresolved product to key 0, raises the visible total from 625 to 655 for this isolated test, and makes 30 explicitly attributable to Unknown product. A production pipeline should additionally quarantine/alert and later restate the foreign key when the legitimate product member arrives.

Cleanup: delete the disposable lab files; do not merge the P999 event into the canonical course fixture.

5. Let the database reject impossible warehouse keys

SQL · controlled FK failure
PRAGMA foreign_keys = ON;INSERT INTO fact_sales(sales_key,date_key,customer_key,product_key,store_key,order_id,line_no,quantity,unit_price,extended_amount,source_batch_id)VALUES(1000,20260920,104,999999,302,'O-INVALID',1,1,10,10,'FAILURE-LAB');

On the stated SQLite harness, this insert fails with a foreign-key constraint error because product key 999999 does not exist. That failure is desirable. On engines that do not enforce such constraints, the equivalent orphan query must be a load/quality assertion.

6. Controlled failure: use only natural keys in facts

A natural-key-only fact appears simpler: store customer_id and product_id directly and join them later. The problem arrives when a natural key is recycled, two source systems use the same value for different entities, or a Type 2 dimension creates multiple historical rows for one natural key. The fact no longer identifies one warehouse dimension row unambiguously.

The repair is to preserve the natural key in the dimension for matching and lineage while storing the resolved surrogate key in the fact. The key lookup is part of the load contract and must be observable/tested.

7. Production judgment and bridge

Unknown-member handling should be governed like any other data-quality policy: define when it is allowed, how it is labeled, how exceptions are counted/alerted, and whether later restatement is required. “Unknown” must not become a permanent hiding place for broken source mappings.

The next lesson compares the logical star with snowflaked and flat-wide alternatives. The goal is not to crown a universal fastest shape, but to separate semantic maintainability from engine-specific physical performance.

Knowledge check

Check your understanding

  1. Why keep natural keys in dimensions if facts join on surrogate keys?
  2. What invariant should every fact foreign key satisfy?
  3. Why is dropping an orphan fact usually worse than mapping it to an explicit Unknown member?
  4. What did the P999 lab prove?
  5. Are database-declared foreign keys sufficient as the only data-quality control on every analytical engine?
Review the answers

1. Natural keys are needed to match source records, reconcile lineage, and group multiple warehouse versions of the same business entity.

2. It must resolve to exactly one valid dimension row according to the load-time/as-of policy.

3. Dropping it distorts totals and hides the exception; an Unknown member preserves the measurement while exposing missing context.

4. That an unresolved dimension lookup can cause silent measurement loss under an inner-join load, while an explicit default member preserves the amount and makes the exception queryable.

5. No. Constraint enforcement varies; explicit orphan/reconciliation tests remain necessary.

Summary and next step

AtlasMart facts now have durable warehouse keys and an explicit exception path for unresolved context. Canonical orphan checks return zero rows, while the isolated P999 lab demonstrates why lossless, observable handling matters.

Next: compare star, snowflake, and flat-wide serving shapes without confusing fewer joins with guaranteed correctness or performance.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.