Range types and exclusion constraints

PostgreSQL · Advanced · 12 min

Double bookings are a classic race condition that an exclusion constraint prevents in one line.

A range type stores an interval of values as one column: int4range, numrange, daterange, tstzrange. Bounds can be inclusive [ or exclusive ); the convention [start, end) makes adjacent ranges meet without overlapping. Since PostgreSQL 14, multiranges hold sets of non-contiguous ranges.

Operators make interval logic simple and correct:

  • && overlaps, @> contains a value or range, <@ is contained by.
  • -|- adjacent, * intersection, + union.
  • lower(r), upper(r), isempty(r).

The killer feature is the exclusion constraint: EXCLUDE USING gist (room WITH =, during WITH &&) means "no two rows may have equal room and overlapping during". Like a unique constraint, it is enforced atomically, so two concurrent bookings for the same slot can't both succeed — something application checks can't guarantee without locking. Equality on a plain column inside a GiST index needs the btree_gist extension.

Ranges are useful for bookings, price validity periods, shift schedules and versioned (temporal) data. PostgreSQL 18 adds temporal PRIMARY KEY/UNIQUE constraints with WITHOUT OVERLAPS, building on the same idea.

Example

CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE booking (
    room   int NOT NULL,
    during tstzrange NOT NULL,
    EXCLUDE USING gist (room WITH =, during WITH &&)
);
INSERT INTO booking VALUES (1, '[2026-06-01 10:00+00, 2026-06-01 12:00+00)');
INSERT INTO booking VALUES (1, '[2026-06-01 12:00+00, 2026-06-01 13:00+00)');  -- ok, adjacent
INSERT INTO booking VALUES (2, '[2026-06-01 11:00+00, 2026-06-01 12:00+00)');  -- ok, other room
INSERT INTO booking VALUES (1, '[2026-06-01 11:30+00, 2026-06-01 12:30+00)');
-- ERROR: conflicting key value violates exclusion constraint "booking_room_during_excl"

SELECT room FROM booking
WHERE during @> '2026-06-01 11:15+00'::timestamptz ORDER BY room;   -- 1, 2

Remember

  • Range types store intervals; use [start, end) bounds
  • && tests overlap, @> tests containment
  • EXCLUDE USING gist prevents overlapping rows atomically

Watch out for

  • Storing start and end in two columns and checking overlaps in application code, which races.

Check yourself

  1. Ranges '[10:00, 12:00)' and '[12:00, 13:00)' for the same room. Does the exclusion constraint reject the second?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.postgresql.org/docs/current/rangetypes.html