Constraints (Rules the Database Enforces)

Problem / Context

Your team keeps shipping validations in app code, but data still goes bad when scripts or new services bypass checks. You want the database to reject invalid rows so bugs stop at the door. PostgreSQL constraints make rules first-class and visible in schema.

Core Concept

Constraints are declarative rules that PostgreSQL enforces automatically. They block bad writes and keep data clean regardless of how it’s written (app, script, or psql).

Core types:

Implementation

1. CHECK (custom rule)

CREATE TABLE products (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  price NUMERIC(10,2) NOT NULL,
  CONSTRAINT chk_products_price_positive CHECK (price > 0)
);

Outcome: inserting a negative price fails.

2. UNIQUE (no duplicates)

CREATE TABLE users (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email TEXT NOT NULL,
  CONSTRAINT uidx_users_email UNIQUE (email)
);

You can also make a unique rule on many columns (for example, one row per (user_id, year)).

3. Naming style

Name constraints for clarity and easy ALTER/DROP:

Example:

CREATE TABLE orders (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id INT NOT NULL,
  total NUMERIC(10,2) NOT NULL,
  CONSTRAINT fk_orders_user_id FOREIGN KEY (user_id) REFERENCES users(id),
  CONSTRAINT chk_orders_total_positive CHECK (total >= 0)
);

4. Deferrable constraints (check at commit)

Hypothetical: you keep a ranked list with a UNIQUE position. You need to swap positions of two items. If uniqueness is checked on each UPDATE, the first update fails. You want the database to allow temporary duplicates during the transaction and only check at COMMIT.

Deferrable makes this work. PostgreSQL checks these constraints at COMMIT, not at each row change.

How to declare

Temporarily defer inside a transaction

BEGIN;
  SET CONSTRAINTS ALL DEFERRED;             -- or a specific name
  -- do your updates/inserts here
COMMIT;                                     -- checks run now

Example A: swapping positions with a deferrable UNIQUE

CREATE TABLE list_items (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  position INT NOT NULL,
  CONSTRAINT uidx_list_items_position UNIQUE (position) DEFERRABLE INITIALLY DEFERRED
);

BEGIN;
  UPDATE list_items SET position = 0 WHERE position = 1; -- temporary duplicate/gap allowed
  UPDATE list_items SET position = 1 WHERE position = 2;
  UPDATE list_items SET position = 2 WHERE position = 0;
COMMIT; -- uniqueness checked here

Without DEFERRABLE, the first UPDATE would violate uniqueness.

Example B: insert child before parent with a deferrable FOREIGN KEY

CREATE TABLE users (
  id INT PRIMARY KEY
);

CREATE TABLE invoices (
  id INT PRIMARY KEY,
  user_id INT NOT NULL,
  CONSTRAINT fk_invoices_user_id
    FOREIGN KEY (user_id) REFERENCES users(id)
    DEFERRABLE INITIALLY DEFERRED
);

BEGIN;
  INSERT INTO invoices (id, user_id) VALUES (1, 100); -- parent 100 not yet present
  INSERT INTO users (id) VALUES (100);                 -- parent arrives later
COMMIT;                                               -- FK checked now and passes

Self-reference also works the same way (a row pointing to another row in the same table). Define the FK as DEFERRABLE to allow a sequence of updates/inserts that is only valid at the end.

Notes

Troubleshooting and migration

5. Partial uniqueness and covering

For patterns like partial unique indexes and covering (INCLUDE) indexes, see the Indexes chapter. That chapter shows how to tailor uniqueness to active rows and how to include extra columns for faster reads.

Variations and Trade‑Offs

Pitfalls

Recap

Optional Exercises

References