Module F-6·15 min read

PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK — letting the database enforce your rules so application code does not have to.

Written by Jatin Jain Saraf · Architect of SupraScan

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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.