PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK — letting the database enforce your rules so application code does not have to.
F-6 — Constraints and Data Integrity
Who this module is for: You completed F-5 and can query data across multiple tables. Now you need to understand how to make the database enforce your rules automatically — so bad data cannot enter even if your application has a bug. Constraints are the database's immune system.
Why Constraints Matter
Imagine a bug in your application that accidentally submits an order with customer_id = 999 — but no customer with ID 999 exists. Without constraints, PostgreSQL happily stores this invalid order. Your data becomes inconsistent: you have orders that reference customers who do not exist.
Or your application has a bug that allows two users to register with the same email address. Now you have duplicate accounts and authentication breaks.
Constraints prevent these problems at the database level — not in your application code, not in your API layer, but at the very last line of defence before data is written to disk.
PRIMARY KEY — Every Row Needs a Unique Identity
A primary key uniquely identifies each row. No two rows can have the same primary key value, and it can never be NULL.
-- Using BIGSERIAL (auto-incrementing, recommended for most tables)
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL
);
-- Using UUID (when you need globally unique IDs)
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Composite primary key (rare — when no single column is unique, but a combination is)
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id) -- each product appears once per order
);
When you insert a row that violates the primary key:
INSERT INTO users (id, email) VALUES (1, 'alice@example.com'); -- works
INSERT INTO users (id, email) VALUES (1, 'bob@example.com'); -- fails!
-- ERROR: duplicate key value violates unique constraint "users_pkey"
-- DETAIL: Key (id)=(1) already exists.
BIGSERIAL vs manual ID: when you use BIGSERIAL, PostgreSQL automatically assigns the next available integer. You never specify id in your INSERT — it is generated for you. This is the correct pattern for most tables.
NOT NULL — Mandatory Fields
NOT NULL prevents a column from ever being left empty.
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL, -- required
price NUMERIC(10,2) NOT NULL, -- required
sku TEXT -- optional (can be NULL)
);
-- This fails:
INSERT INTO products (name) VALUES ('Keyboard');
-- ERROR: null value in column "price" of relation "products" violates not-null constraint
-- This works:
INSERT INTO products (name, price) VALUES ('Keyboard', 129.99);
-- sku is left as NULL — that is fine
Rule: if a column must always have a value, add NOT NULL. If the value is sometimes unknown or optional, leave the column nullable. Be intentional — a column without NOT NULL silently accepts missing data.
UNIQUE — No Duplicates Allowed
UNIQUE ensures no two rows have the same value in that column (or combination of columns).
-- Single column unique
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE -- no two users can have the same email
);
-- Unique constraint on multiple columns (combination must be unique)
CREATE TABLE team_memberships (
user_id BIGINT NOT NULL,
team_id BIGINT NOT NULL,
UNIQUE (user_id, team_id) -- a user can only be in the same team once
);
When a unique constraint is violated:
INSERT INTO users (email) VALUES ('alice@example.com'); -- works
INSERT INTO users (email) VALUES ('alice@example.com'); -- fails!
-- ERROR: duplicate key value violates unique constraint "users_email_key"
-- DETAIL: Key (email)=(alice@example.com) already exists.
UNIQUE vs PRIMARY KEY:
PRIMARY KEY= unique + not null + the table's main identifier (one per table)UNIQUE= no duplicates, but NULLs are allowed (and multiple NULLs count as different values in PostgreSQL)
FOREIGN KEY — Referential Integrity
A foreign key ensures that a value in one table references a real row in another table. It is the constraint that makes joins meaningful.
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id),
total NUMERIC(10,2) NOT NULL
);
-- This fails because customer_id 999 does not exist:
INSERT INTO orders (customer_id, total) VALUES (999, 49.99);
-- ERROR: insert or update on table "orders" violates foreign key constraint
-- DETAIL: Key (customer_id)=(999) is not present in table "customers".
What happens when you delete a referenced row?
By default, trying to delete a customer who has orders fails:
DELETE FROM customers WHERE id = 1;
-- ERROR: update or delete on table "customers" violates foreign key constraint
-- DETAIL: Key (id)=(1) is still referenced from table "orders".
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion