Constraints
Enforcing uniqueness with UNIQUE
Some values should be allowed to appear only once in a table.
For example, our application should not have two customers with the same email address.
The customers.email column already has a UNIQUE constraint.
To see an existing value, run:
123SELECT id, emailFROM customersWHERE id = 1;You should see:
123 id | email----+----------------------- 1 | Customer1@Example.comNow try inserting another customer with the same email:
1234567891011121314INSERT INTO customers ( id, email, full_name, country, created_at)VALUES ( 900001, 'Customer1@Example.com', 'Another Customer', 'US', '2025-01-01');PostgreSQL rejects the insert.
The existing row already contains:
1Customer1@Example.comand the UNIQUE constraint prevents another row from using the same value.
What does UNIQUE do?
A UNIQUE constraint requires the values covered by the constraint to be unique across rows.
For example:
1email TEXT UNIQUEmeans PostgreSQL will not allow two rows to contain the same non-NULL email value.
A simplified view looks like this:
The database itself now protects the uniqueness rule.
UNIQUE and PRIMARY KEY
A primary key also requires unique values, but a primary key has an additional role: it identifies each row in the table.
For example:
1customers.idis the primary key.
While:
1customers.emailis another value that must be unique.
A table can have only one primary key, but it can have multiple UNIQUE constraints.
For example, a users table might have:
123id → PRIMARY KEYemail → UNIQUEusername → UNIQUEEach constraint represents a different rule about the data.
UNIQUE can cover multiple columns
Sometimes no single column needs to be unique, but a combination of values must be.
Imagine an application has a table containing products that customers have saved:
123456CREATE TABLE saved_products ( id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, product_id INTEGER NOT NULL, UNIQUE (customer_id, product_id));The same customer can save many products:
12customer 1 + product 10customer 1 + product 20and the same product can be saved by many customers:
12customer 1 + product 10customer 2 + product 10But this combination cannot appear twice:
12customer 1 + product 10customer 1 + product 10The unique rule applies to the pair:
1customer_id + product_idrather than either column individually.
UNIQUE and NULL
By default, PostgreSQL treats NULL values as distinct when enforcing a UNIQUE constraint.
For example, imagine:
1phone TEXT UNIQUEIf phone allows NULL, multiple rows can normally contain:
123NULLNULLNULLbecause PostgreSQL does not treat those NULL values as equal duplicates for the purpose of the constraint.
When an application needs NULL values to be treated as duplicates as well, PostgreSQL also supports:
1UNIQUE NULLS NOT DISTINCT (phone)With that form, only one row can contain NULL for the constrained value.
For many application columns such as email, you will instead commonly combine:
1NOT NULLand:
1UNIQUEso the value must both exist and be unique.
UNIQUE creates an index
When you create a UNIQUE constraint, PostgreSQL automatically creates a unique B-tree index to enforce it.
That is why our customers.email column already has an index even though we didn't explicitly run:
1CREATE INDEX ...The constraint and index have related but different roles.
The constraint expresses the data rule:
1No two customers can have the same email.The index gives PostgreSQL an efficient way to enforce that rule.
Why application checks are not enough
Imagine an application creates a user like this:
121. Search for the email.2. If it doesn't exist, insert the user.At first, this might seem enough to prevent duplicates.
But two requests could happen at almost the same time:
1234567891011Request A Request Bcheck email→ not found check email → not foundinsert customer insert customerBoth requests saw that the email was available before either insert completed.
If the database has no uniqueness constraint, both rows could be inserted.
This is a race condition.
With:
1email TEXT UNIQUEPostgreSQL guarantees that only one of the inserts can succeed.
The other insert is rejected because the uniqueness rule has been violated.
A simplified view looks like this:
This is an important reason to enforce important invariants inside the database rather than relying only on application code.
Application validation is still useful
The application can still check whether an email already exists.
Doing so may allow it to give the user a friendly message such as:
1An account with this email already exists.But the database constraint remains the final guarantee.
The correct pattern is usually:
123application validation +database constraintrather than choosing one or the other.
UNIQUE expresses a data rule
A regular index is primarily a performance structure.
A UNIQUE constraint expresses something about the correctness of the data.
For example:
1email TEXT UNIQUEdoes not mean:
1We want email lookups to be faster.It means:
1Duplicate email values are not valid data.PostgreSQL happens to create an index to enforce that rule efficiently.
The important point is:
Use a `UNIQUE` constraint when uniqueness is part of the data model, not merely because you want a unique index.