Unique and Exclusion Constraints in Postgres
In Postgres, most constraints work on a single row, like check
and not-null
. Where we finally start to work across multiple rows is with unique constraints.
You can read a unique constraint as “no two rows have the same value for these columns”.
1create table georgia_bulldogs (2 name text primary key,3 number int not null unique4);5
6insert into georgia_bulldogs (name, number)7values ('Carson Beck', 15), ('Random Freshman', 15);8-- ERROR: duplicate key value violates unique constraint "georgia_bulldogs_number_key"9-- DETAIL: Key (number)=(15) already exists.
Partial Indexes
Now, you can constrain columns for a subset of rows by using a partial index.
1create table georgia_bulldogs (2 name text primary key,3 number int not null,4 starting boolean not null default false5);6
7create unique index on georgia_bulldogs (number) where starting is true;8
9insert into georgia_bulldogs (name, number, starting)10values ('Carson Beck', 15, true), ('Random Freshman', 15, false);11-- no problem12
13insert into georgia_bulldogs (name, number, starting)14values ('Trevor Etienne', 1, true), ('Other Random Freshman', 1, true);15-- ERROR: duplicate key value violates unique constraint "georgia_bulldogs_number_idx"16-- DETAIL: Key (number)=(1) already exists.
Handling of Nulls
No discussion of postgres unique constraints would be complete without mentioning nulls not distinct
. (Note - I recently learned I’m not qualified to use nulls).
By default, two null values are not considered equal in a unique constraint.
1create table a (2 value text unique3);4
5insert into a (value) values (null), (null);6-- all good
This behavior can be changed with nulls not distinct
.
1create table a (2 value text unique nulls not distinct3);4
5insert into a (value) values (null), (null);6-- ERROR: duplicate key value violates unique constraint "a_value_key"7-- DETAIL: Key (value)=(null) already exists.
Exclusion Constraints
An exclusion constraint is a generalized version of a unique constraint. They are all about preventing overlap between two rows, any form of overlap.
Reimplement a Unique Constraint With an Exclusion Constraint
1create table georgia_bulldogs (2 name text primary key,3 number int not null,4 exclude (number with =)5);6
7insert into georgia_bulldogs (name, number)8values ('Carson Beck', 15), ('Random Freshman', 15);9-- ERROR: conflicting key value violates exclusion constraint "georgia_bulldogs_number_excl"10-- DETAIL: Key (number)=(15) conflicts with existing key (number)=(15).
However, with an exclusion constraint, we can use any operator that is:
- commutative (
a <operator> b = b <operator> a
) - boolean
- searchable by the given index
(List taken from this source).
In practice, the most useful one is the overlap operator, &&
.
1-- true2select daterange '[2023-01-01,2024-01-01)' && daterange '[2023-08-01,2024-05-29)';3-- false4select daterange '[2023-01-01,2024-01-01)' && daterange '[2024-08-01,2025-05-29)';
Scheduling - Overlapping Date Ranges
1-- need to use = operator for integers with gist2create extension if not exists btree_gist;3
4create table rooms (5 id serial primary key6);7
8create table reservations (9 room_id int references rooms(id),10 time tstzrange not null,11 exclude using gist (room_id with =, time with &&)12);13
14insert into rooms default values;15
16insert into reservations (room_id, time)17select id, tstzrange(now(), now() + '5 days')18from rooms;19
20insert into reservations (room_id, time)21select id, tstzrange(now() + '3 days', now() + '7 days')22from rooms;
And we get an error that looks like this:
1ERROR: conflicting key value violates exclusion constraint "reservations_room_id_time_excl"2DETAIL: Key (room_id, "time")=(1, ["2024-10-18 05:44:11.220589-04","2024-10-22 05:44:11.220589-04")) conflicts with existing key (room_id, "time")=(1, ["2024-10-15 05:44:11.216798-04","2024-10-20 05:44:11.216798-04")).
Resources
Wow! You read the whole thing. People who make it this far sometimes
want to receive emails when I post something new.
I also have an RSS feed.