Exclusion Constraint Conflicts With Existing Rows
Two related failures, both SQLSTATE 23P01. At schema-change time, ALTER TABLE ... ADD CONSTRAINT ... EXCLUDE ... fails with 'could not create exclusion...
In 10 seconds
You want to make double-booking impossible: no two reservations for the same room may have overlapping time ranges. The right tool is an exclusion constraint -- EXCLUDE USING gist (room_id WITH =, during WITH &&). But ALTER TABLE ... ADD CONSTRAINT fails immediately with 'could not create exclusion constraint', because the table already contains overlapping bookings that slipped in before the rule existed. PostgreSQL will not create a constraint that the current data violates. You must find and resolve the existing overlaps first, then add the constraint -- which from then on rejects any overlapping insert at write time.
Why This Happens
An exclusion constraint generalizes UNIQUE: instead of forbidding two rows that are equal, it forbids two rows that are 'true' together under a set of operators -- here equal room_id (=) and overlapping ranges (&&), backed by a gist index (btree_gist supplies gist support for the scalar room_id). Creating the constraint builds that index and checks every existing row against every other; if any pair satisfies all the operators it is a violation, so the create aborts with 23P01. The data predates the rule, so the overlaps are already there -- exactly like trying to add UNIQUE to a column that already has duplicates.
Diagnose
Free Tier -- Basic Approach
Count the overlapping pairs that block the constraint with a self-join on the same room and an overlapping range.
SELECT count(*) AS overlapping_pairs FROM room_bookings a JOIN room_bookings b ON a.room_id = b.room_id AND a.id < b.id AND a.during && b.during;
overlapping_pairs
-------------------
5
(1 row)
SELECT count(*) AS overlapping_pairs FROM room_bookings a JOIN room_bookings b ON a.room_id = b.room_id AND a.id < b.id AND a.during && b.during. Five pairs overlap -- the exact number of conflicts you must resolve before the EXCLUDE constraint can be created.
Fix
Resolve the conflicts, then add the constraint. Here the overlapping additions are the later booking of each pair; deleting them leaves one clean booking per room so the ADD CONSTRAINT validates and succeeds.
DELETE FROM room_bookings WHERE id IN (
SELECT b.id FROM room_bookings a JOIN room_bookings b
ON a.room_id = b.room_id AND a.id < b.id AND a.during && b.during
);
ALTER TABLE room_bookings ADD CONSTRAINT no_overlap EXCLUDE USING gist (room_id WITH =, during WITH &&);
DELETE 5 ALTER TABLE
DELETE FROM room_bookings WHERE id IN (SELECT b.id FROM room_bookings a JOIN room_bookings b ON a.room_id = b.room_id AND a.id < b.id AND a.during && b.during) removes the 5 later bookings (DELETE 5). Then ALTER TABLE room_bookings ADD CONSTRAINT no_overlap EXCLUDE USING gist (room_id WITH =, during WITH &&) completes (ALTER TABLE) because no overlapping pair remains.
Verify
Confirm the constraint exists, an overlapping insert is rejected with 23P01, and a non-overlapping insert is accepted -- proving the rule blocks exactly the double-bookings and nothing else.
SELECT conname, contype FROM pg_constraint WHERE conrelid = 'room_bookings'::regclass AND contype = 'x';
conname | contype ------------+--------- no_overlap | x (1 row)
INSERT INTO room_bookings(room_id, during) VALUES (1, tsrange(TIMESTAMP '2025-01-01 02:30', TIMESTAMP '2025-01-01 03:30'));
ERROR: 23P01: conflicting key value violates exclusion constraint "no_overlap" DETAIL: Key (room_id, during)=(1, ["2025-01-01 02:30:00","2025-01-01 03:30:00")) conflicts with existing key (room_id, during)=(1, ["2025-01-01 02:00:00","2025-01-01 03:00:00")). SCHEMA NAME: public TABLE NAME: room_bookings CONSTRAINT NAME: no_overlap LOCATION: check_exclusion_or_unique_constraint, execIndexing.c:874
INSERT INTO room_bookings(room_id, during) VALUES (1, tsrange(TIMESTAMP '2025-06-01 00:00', TIMESTAMP '2025-06-01 01:00'));
INSERT 0 1
SELECT conname, contype FROM pg_constraint WHERE conrelid = 'room_bookings'::regclass AND contype = 'x' shows no_overlap with contype x. Inserting an overlapping booking for room 1 fails with 'conflicting key value violates exclusion constraint', while a non-overlapping booking for room 1 succeeds (INSERT 0 1).
⏪ Rollback
The failed ADD CONSTRAINT is atomic -- it leaves no half-built constraint or index, so there is nothing to clean up after the error. The fix deletes the overlapping additions; if you need them back, restore from backup before re-running (the recipe deletes the later booking of each conflicting pair). Dropping the constraint afterward is ALTER TABLE room_bookings DROP CONSTRAINT no_overlap, which also removes its backing index. No other table is touched.
📦 Version Matrix (PG 14--18)
Try the create-over-conflicts and the resolve-then-create on PostgreSQL 14 through 18. Every version refuses the constraint while overlaps exist and accepts it once they are gone.
| Version | Behavior | Version Notes |
|---|---|---|
PG 14.23 |
broken=could not create exclusion constraint | ✗ Not available -- use stats_reset age |
PG 15.18 |
broken=could not create exclusion constraint | ✗ Not available -- use stats_reset age |
PG 16.14 |
broken=could not create exclusion constraint | ✗ Not available -- use stats_reset age |
PG 17.10 |
broken=could not create exclusion constraint | ✗ Not available -- use stats_reset age |
PG 18.4 |
broken=could not create exclusion constraint | ✗ Not available -- use stats_reset age |
⚠ Common Mistakes
Stop this incident before it pages you
The free runbook clears today's failure. Pro adds the standing guardrails -- the alert query, the dashboard, and the managed-service knobs -- so it never becomes an incident again.
- ✓ A ready-to-paste alert query with the right thresholds.
- ✓ The topology change that prevents recurrence.
- ✓ The exact knobs on RDS, Azure Flexible Server & Cloud SQL.
Frequently Asked Questions
Why can't I add the exclusion constraint?
Because the table already contains rows that violate it -- here two bookings for the same room with overlapping times. Creating an exclusion constraint validates all existing data, and any overlapping pair makes the create fail with 23P01 'could not create exclusion constraint'. Resolve the existing overlaps first, then the create succeeds.
How do I find the conflicts before adding the constraint?
Self-join the table on the equality column and the overlap operator: SELECT ... FROM room_bookings a JOIN room_bookings b ON a.room_id = b.room_id AND a.id < b.id AND a.during && b.during. The a.id < b.id clause lists each pair once. That gives you the exact bookings to cancel, move, or trim.
Do I really need btree_gist?
Yes, when the constraint mixes equality on a scalar (room_id =) with a range overlap (during &&) in a single gist index. Plain gist has no default operator class for ordinary scalars; btree_gist adds it. If you exclude purely on ranges (during WITH && only), the built-in range gist support is enough and the extension is not required.
What's the difference between the two 23P01 messages?
'could not create exclusion constraint' is raised while building the constraint over data that already conflicts -- a schema-time error. 'conflicting key value violates exclusion constraint' is raised at run time when a new INSERT/UPDATE would overlap an existing row -- the constraint enforcing itself. Same SQLSTATE, different moment: one blocks the rule, the other is the rule working.