Constraints
A constraint is a rule attached to a table that PostgreSQL enforces on every INSERT, UPDATE, and sometimes DELETE — no exceptions, no matter which application, script, or person is doing the writing. Primary keys and foreign keys, covered on their own pages, are two specific kinds of constraint. This page rounds out the full picture: every constraint type PostgreSQL supports, and why enforcing rules at the database level matters even when the application layer already validates the same thing.
The constraint types
Constraint | Enforces |
|---|---|
PRIMARY KEY | Column(s) uniquely identify each row; implies NOT NULL + UNIQUE |
FOREIGN KEY | A column's value must match a row in another table (referential integrity) |
UNIQUE | No two rows can have the same value in the column(s) |
NOT NULL | The column can never be NULL |
CHECK | A boolean expression must hold true for every row |
EXCLUSION | No two rows may satisfy a given operator comparison at the same time (e.g. no overlapping ranges) |
The everyday constraint types in one table
CREATE TABLE products ( product_id SERIAL PRIMARY KEY, sku TEXT UNIQUE NOT NULL, name TEXT NOT NULL, price NUMERIC(10, 2) NOT NULL CHECK (price >= 0), category_id INTEGER REFERENCES categories (category_id) );
EXCLUSION constraints — a distinctive PostgreSQL feature
The first five constraint types have equivalents in essentially every relational database. EXCLUSION constraints are different: they are a genuinely PostgreSQL feature that generalizes UNIQUE to arbitrary operators, not just equality. A UNIQUE constraint says "no two rows can be equal on this column." An EXCLUDE constraint says "no two rows can satisfy this operator" for any operator with an appropriate index support — most commonly used to forbid overlapping time or numeric ranges.
Preventing overlapping room reservations
CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE TABLE reservations ( reservation_id SERIAL PRIMARY KEY, room_id INTEGER NOT NULL, during TSRANGE NOT NULL, EXCLUDE USING GIST (room_id WITH =, during WITH &&) );
This says: for a given room_id, no two reservations may have overlapping during ranges. The Range Types page walks through this exact reservation-overlap example in full, including why it needs a GiST index to work.
Naming constraints explicitly
Every constraint gets a name whether you provide one or not — if you don't, PostgreSQL generates one like products_price_check. The trouble with an auto-generated name is that it shows up verbatim in error messages, and products_price_check is far less useful to a developer or a support engineer than a name that states the actual rule.
A named CHECK constraint gives a clearer error
CREATE TABLE products ( product_id SERIAL PRIMARY KEY, price NUMERIC(10, 2) NOT NULL, CONSTRAINT price_must_be_non_negative CHECK (price >= 0) ); INSERT INTO products (price) VALUES (-5);
ERROR: new row for relation "products" violates check constraint "price_must_be_non_negative" DETAIL: Failing row contains (1, -5.00).
price_must_be_non_negative reads as a sentence; anyone debugging the failure immediately knows what rule was broken without having to go look up the table definition.
Why enforce rules in the database at all
Application code can validate input too, and often should — for better error messages shown to a user, for instance. But application-level validation only protects the paths that go through that particular application. A one-off migration script, an admin running a manual UPDATE, a second service that gets added later and talks to the same database, a bulk import — none of those go through the application's validation code, and any one of them can silently corrupt the data if the database itself doesn't also enforce the rule.
A constraint declared in the schema is the last line of defense: no matter what wrote the row, or forgot to check something, the constraint holds. It is also self-documenting — anyone reading the CREATE TABLE statement sees the business rules the data must satisfy, rather than having to trace through application code scattered across the codebase.
PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, and CHECK cover the everyday cases.
EXCLUSION constraints generalize UNIQUE to arbitrary operators — most notably, preventing overlapping ranges.
Name constraints explicitly (CONSTRAINT name ...) so error messages describe the rule, not a generated identifier.
Database-level constraints are the last line of defense against writes that bypass application validation entirely.