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.
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:
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.
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)).
Name constraints for clarity and easy ALTER/DROP:
pk_<table>uidx_<table>_<col>chk_<table>_<meaning>fk_<table>_<refcol>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)
);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 nowExample 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 hereWithout 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 passesSelf-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
Error: “constraint XYZ is not deferrable”
SELECT conname, deferrable, initially_deferred
FROM pg_constraint
WHERE conrelid = 'your_table'::regclass
AND conname = 'your_constraint_name';SET CONSTRAINTS works only inside a transaction and only for deferrable constraints:
BEGIN;
SET CONSTRAINTS your_constraint_name DEFERRED;
-- do work
COMMIT;Make a FOREIGN KEY deferrable (low risk path)
ALTER TABLE invoices
DROP CONSTRAINT fk_invoices_user_id;
ALTER TABLE invoices
ADD CONSTRAINT fk_invoices_user_id
FOREIGN KEY (user_id) REFERENCES users(id)
DEFERRABLE INITIALLY IMMEDIATE
NOT VALID; -- skip full check now
ALTER TABLE invoices
VALIDATE CONSTRAINT fk_invoices_user_id; -- check existing rows onlineMake a UNIQUE constraint deferrable
PostgreSQL does not support a deferrable unique index. Deferrable applies to the table constraint.
You cannot turn an existing unique index into a deferrable constraint in place.
Simple path (maintenance window):
ALTER TABLE users DROP CONSTRAINT uidx_users_email; -- old unique
ALTER TABLE users
ADD CONSTRAINT uidx_users_email
UNIQUE (email) DEFERRABLE INITIALLY DEFERRED; -- new deferrable uniqueLower-lock idea (plan carefully): build a second unique index concurrently, switch to a deferrable unique constraint during a short window, then drop the old index.
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.