Schema Design and Constraints
Let the database refuse what the pipeline shouldn't write
Primary and foreign keys, unique constraints, NOT NULL and CHECKs are the enforceable guardrails that stop bad rows from ever entering your tables.
What you will learn
- Design primary/foreign keys, unique, NOT NULL and CHECK constraints
- Normalize to 3NF in OLTP without over-normalizing analytics
- Use DDL migrations with constraints as gates
What is this?
The schema is the shape of a table: types, keys and constraints. Constraints are the database-level guarantees — a unique index stops duplicate natural keys, NOT NULL rejects missing mandatory fields, CHECK rejects invalid ranges — before any app or pipeline can write corruption.
Why do you need it?
- Enforced constraints turn data bugs into loud insert failures at the boundary, not silent drift downstream.
- Primary/foreign keys make integrity explicit and give the planner the cardinality info it needs.
- A well-designed schema is documentation you can't forget to read.
Mental model
A building's load-bearing walls
Schema constraints
You can't remove a load-bearing wall without the building failing — that's a constraint. Removing it 'temporarily' (a missing UNIQUE or FK) holds the structure up until, one day, it doesn't.
How it works
- PRIMARY KEY implies uniqueness + NOT NULL and is the row identity.
- FOREIGN KEY limits a column to existing parent keys — orphans are rejected at write.
- UNIQUE enforces no duplicates on non-key columns (email, natural identifiers).
- NOT NULL and CHECK (amount >= 0) restrict value domains.
- Indexes materialize keys/uniques; each adds the write tax a primary key always deserves.
Constraint placement around a table.
Examples
A guarded fact table
CREATE TABLE orders ( order_id BIGINT PRIMARY KEY, -- row identity customer_id BIGINT NOT NULL REFERENCES customers(id), amount NUMERIC(12,2) CHECK (amount >= 0), status TEXT NOT NULL CHECK (status IN ('pending','paid','refunded')), created_at TIMESTAMPTZ NOT NULL);CREATE UNIQUE INDEX uq_orders_native ON orders (native_ref);Every guarantee is enforced in-engine: identity, referential integrity, domain ranges and dedup.
In production
- A dedup layer relies on a UNIQUE constraint on the natural key so a double-load fails instead of duplicating.
- An orders model has FKs to customers/items so orphan rows can't exist to confuse joins.
- A CHECK(amount >= 0) rejects negative amounts at the boundary, so finance totals are trustworthy.
Best practices
- Enforce identity and referential integrity at the DB, not in application code.
- Normalize source-of-record to 3NF; denormalize deliberately in analytics marts.
- Version schema changes with migrations and review N+1-FK implications.
- Index the columns your hottest WHERE uses; let the PK drive identity, not semantics.
Common mistakes
- Making the natural key the PK so a business-meaning change rewrites history.
- Forgetting FK constraints 'for speed', then debugging orphan joins quarterly.
- CHECK-ing a range that drift makes obsolete, and disabling it 'temporarily' forever.
- Over-normalizing an analytics mart so every query needs six joins.
Common issues
UNIQUE constraint violated on retryNon-idempotent load re-inserted an already-loaded natural key
Make loads upsert or dedupe first; the constraint is doing its job.
FK constraint blocked a loadParent rows didn't exist yet in the referred table
Order the load parents-before-children or defer the constraint to commit.
Debugging tips
- A failing insert with a constraint error names the exact violated rule — read it before investigating.
- Orphan rows appearing = an FK was bypassed or a legacy load ran without it.
- When a check 'blocks a legit value', re-baseline the domain after reviewing with owners.
Interview checkpoint
Hands-on lab
- Write DDL with PK, FK, UNIQUE, NOT NULL and CHECK constraints
- Prove the database rejects invalid writes loudly
- Add a natural-key unique guard
Design a Constrained Orders Schema
30 min · intermediate
Quick quiz
Which constraint prevents two rows with the same email?
A NOT NULL constraint correctly models 'revenue could be unknown'.