devnotes.

Constraints

Constraints are rules the database enforces, so invalid data cannot be stored no matter which application writes it.

ConstraintGuarantees
NOT NULLA value is always present
UNIQUENo two rows share the value
PRIMARY KEYUnique and not null
FOREIGN KEYThe value exists in the referenced table
CHECKA custom condition holds
EXCLUDENo two rows satisfy a given operator — Postgres only
CREATE TABLE courses (
  id        integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code      varchar(10) NOT NULL UNIQUE,
  title     text        NOT NULL,
  credits   smallint    NOT NULL DEFAULT 3,
  seats     smallint    NOT NULL DEFAULT 40,
  dept_code varchar(10) NOT NULL,

  CONSTRAINT chk_credits CHECK (credits BETWEEN 1 AND 6),
  CONSTRAINT chk_seats   CHECK (seats > 0),
  CONSTRAINT uq_dept_title UNIQUE (dept_code, title),
  CONSTRAINT fk_course_dept FOREIGN KEY (dept_code)
    REFERENCES departments(code) ON UPDATE CASCADE ON DELETE RESTRICT
);

Name your constraints — the name appears in the error message your application will have to interpret.

Adding and removing

ALTER TABLE courses ADD CONSTRAINT chk_title CHECK (length(title) >= 3);
ALTER TABLE courses DROP CONSTRAINT chk_title;
ALTER TABLE courses ALTER COLUMN title SET NOT NULL;

On a large table, add the constraint as NOT VALID first and validate separately — that avoids a long exclusive lock:

ALTER TABLE courses ADD CONSTRAINT chk_seats CHECK (seats > 0) NOT VALID;
ALTER TABLE courses VALIDATE CONSTRAINT chk_seats;

NOT VALID means existing rows are not checked but all new writes are.

UNIQUE and NULL

UNIQUE allows multiple NULLs by default, because NULL is not equal to anything. Since Postgres 15 you can change that:

CREATE TABLE t (email text UNIQUE NULLS NOT DISTINCT);

Partial unique indexes

A uniquely Postgres solution — enforce uniqueness only over part of the table:

-- only one active subscription per user
CREATE UNIQUE INDEX uq_active_sub ON subscriptions (user_id) WHERE status = 'active';

-- soft-deleted rows do not block re-using an email
CREATE UNIQUE INDEX uq_live_email ON users (email) WHERE deleted_at IS NULL;

EXCLUDE constraints

An exclusion constraint generalises UNIQUE to any operator. The classic use is preventing overlapping bookings, which no other mainstream database expresses so directly:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_bookings (
  id     integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  room   integer NOT NULL,
  during tstzrange NOT NULL,
  EXCLUDE USING gist (room WITH =, during WITH &&)
);

Two rows with the same room and overlapping time ranges are now impossible.

Deferrable constraints

A deferrable constraint is checked at COMMIT instead of per statement, which lets you make temporarily inconsistent changes inside a transaction:

ALTER TABLE enrollments
  ADD CONSTRAINT fk_student FOREIGN KEY (student_id) REFERENCES students(id)
  DEFERRABLE INITIALLY DEFERRED;

BEGIN;
  -- swap two primary keys without tripping the FK mid-transaction
  UPDATE students SET id = 999 WHERE id = 1;
  UPDATE students SET id = 1   WHERE id = 2;
COMMIT;

Inspecting constraints

\d courses

SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'courses'::regclass;

Why constraints beat application validation

Application code changes, teams change, and data arrives from scripts, migrations and admins who never touch your validation layer. A constraint is the one rule that is always enforced. Validate in the app for friendly messages, and in the database for correctness.

PostgreSQL Constraints: NOT NULL, UNIQUE, CHECK and EXCLUDE | DevNotes