Chapter 11: Keys, Constraints, and Referential Integrity
Constraints are rules enforced by the database engine. No matter how buggy your application code is, constraints guarantee your data stays valid. They're your last line of defense.
Types of Keys
Primary Key
Uniquely identifies each row. Must be NOT NULL and UNIQUE. One per table.
-- Single-column PK
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT
);
-- Composite PK (multiple columns together form the key)
CREATE TABLE enrollments (
student_id INTEGER,
course_id INTEGER,
grade CHAR(1),
PRIMARY KEY (student_id, course_id)
);
Foreign Key
References a primary key in another table. Enforces referential integrity — you can't have orphan records.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
total NUMERIC(10,2),
CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT -- prevent deleting customer with orders
ON UPDATE CASCADE -- if customer.id changes, update here too
);
Foreign Key Actions
| Action | ON DELETE behavior | Use When |
|---|---|---|
| RESTRICT | Block the delete | Can't delete parent if children exist |
| CASCADE | Delete children too | Children meaningless without parent |
| SET NULL | Set FK to NULL | Relationship is optional |
| SET DEFAULT | Set FK to default value | Reassign to a default parent |
| NO ACTION | Like RESTRICT (checked at end of transaction) | Default behavior |
Natural Key vs Surrogate Key
| Type | Example | Pros | Cons |
|---|---|---|---|
| Natural | email, SSN, ISBN | Meaningful, no extra column | Can change, may be large, privacy issues |
| Surrogate | SERIAL id, UUID | Stable, small, never changes | Meaningless, extra column |
💡 Best Practice
Use surrogate keys (SERIAL or UUID) as primary keys. Add UNIQUE constraints on natural keys (email, username). This gives you stable references AND business-level uniqueness.
Constraint Types
CREATE TABLE products (
id SERIAL PRIMARY KEY,
sku VARCHAR(20) UNIQUE NOT NULL, -- unique + required
name TEXT NOT NULL, -- required
price NUMERIC(10,2) CHECK (price > 0), -- custom validation
category TEXT CHECK (category IN ('electronics', 'clothing', 'food')),
weight_kg NUMERIC CHECK (weight_kg >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Multi-column constraint
CONSTRAINT valid_dates CHECK (sale_end > sale_start)
);
Unique Constraints and Indexes
-- Single column unique
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
-- Multi-column unique (combination must be unique)
ALTER TABLE enrollments ADD CONSTRAINT unique_enrollment
UNIQUE (student_id, course_id);
-- Partial unique (unique only among active users)
CREATE UNIQUE INDEX idx_active_email
ON users (email) WHERE active = true;
Exclusion Constraints (PostgreSQL)
Prevent overlapping ranges — useful for scheduling:
-- No overlapping room bookings
CREATE TABLE bookings (
room_id INTEGER,
time_range TSTZRANGE,
EXCLUDE USING GIST (room_id WITH =, time_range WITH &&)
);
-- && means "overlaps" — this prevents double-booking a room
Referential Integrity in Practice
What happens when you try to violate constraints:
INSERT INTO orders (customer_id, total) VALUES (999, 50.00);
→ ERROR: insert or update on table "orders" violates foreign key
constraint "fk_customer". Key (customer_id)=(999) is not present
in table "customers".
DELETE FROM customers WHERE id = 1; -- customer has orders
→ ERROR: update or delete on table "customers" violates foreign key
constraint "fk_customer" on table "orders". Key (id)=(1) is still
referenced from table "orders".
The database REFUSES the operation. Data stays consistent.
Key Takeaways
- PRIMARY KEY = unique + not null identifier for each row
- FOREIGN KEY = enforces relationships between tables (no orphans)
- Use surrogate keys (SERIAL/UUID) as PK, natural keys as UNIQUE constraints
- ON DELETE CASCADE/RESTRICT controls what happens when parent is deleted
- CHECK constraints validate business rules at the database level
- Constraints are your safety net — they catch bugs your application misses