Exclusion Constraints sind ein Postgres-Spezial-Feature: sie verhindern, dass zwei Zeilen einen Konflikt nach einem frei wählbaren Operator haben. Klassisches Beispiel: keine zwei Buchungen für den selben Raum dürfen sich zeitlich überschneiden. UNIQUE prüft Gleichheit — EXCLUDE prüft beliebige Operatoren wie && (Range-Overlap).

Das Standard-Beispiel: Buchungs-Überschneidung

SQL Raum-Reservierungen ohne Konflikt
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_bookings (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    room_id bigint NOT NULL,
    during  tstzrange NOT NULL,
    EXCLUDE USING gist (
        room_id  WITH =,
        during   WITH &&
    )
);

Was steht da:

  • room_id WITH = — wenn zwei Zeilen die selbe room_id haben…
  • during WITH && — und sich zeitlich überschneiden (&& = Range-Overlap-Operator)
  • …dann ist das ein Konflikt → INSERT/UPDATE wird abgelehnt.

Test:

SQL
INSERT INTO room_bookings (room_id, during) VALUES
    (1, '[2026-05-07 10:00, 2026-05-07 12:00)');   -- ok

INSERT INTO room_bookings (room_id, during) VALUES
    (1, '[2026-05-07 13:00, 2026-05-07 15:00)');   -- ok, kein Overlap

INSERT INTO room_bookings (room_id, during) VALUES
    (1, '[2026-05-07 11:30, 2026-05-07 14:00)');   -- ERROR: conflicting key

Der dritte Insert scheitert — er überschneidet sich mit dem ersten.

Warum btree_gist?

Der GiST-Index unterstützt nativ Range-Operatoren (&&), aber nicht den Gleichheits-Operator auf einfachen Typen wie bigint.

Die btree_gist-Extension lehrt den GiST-Index, auch = auf Standard-Typen zu indizieren — damit kann ein einziger GiST-Index beide Spalten (room_id = und during &&) gemeinsam abdecken.

Ohne btree_gist würde der Constraint nur funktionieren, wenn alle Spalten Range-Typen sind oder Postgres-spezielle Typen wie inet.

Range-Typen im Überblick

EXCLUDE-Constraints arbeiten typisch mit Range-Typen:

TypBedeutung
int4range, int8range, numrangeNumerische Ranges
tsrangeRange über timestamp
tstzrangeRange über timestamptz (timezone-aware)
daterangeRange über date

Range-Literale-Beispiele:

SQL
'[10, 20)'::int4range          -- 10 ≤ x < 20
'(2026-01-01, 2026-12-31]'::daterange   -- exklusiv unten, inklusiv oben
'[2026-05-07 10:00, 2026-05-07 12:00)'::tstzrange

Eckige Klammern: inklusiv. Runde Klammern: exklusiv. Mehr im Artikel Ranges.

Operator-Wahl

Der WITH-Operator definiert, was als Konflikt gilt:

OperatorBedeutungUse-Case
=Gleichheitwie UNIQUE-Logik
&&Range-OverlapZeit-/Wertebereich-Überschneidung
<>Ungleichheit„alle müssen gleich sein"
~Pattern-Matchseltener

EXCLUDE mit = ist äquivalent zu UNIQUE. Der spannende Use-Case ist && für Ranges.

Praxis-Beispiele

Hotel-Zimmer pro Person

SQL
CREATE TABLE stays (
    id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    guest_id  bigint NOT NULL,
    room_id   bigint NOT NULL,
    during    tstzrange NOT NULL,
    -- Ein Gast kann nicht in zwei Zimmern gleichzeitig sein
    EXCLUDE USING gist (guest_id WITH =, during WITH &&),
    -- Ein Zimmer kann nicht doppelt belegt sein
    EXCLUDE USING gist (room_id  WITH =, during WITH &&)
);

Zwei separate EXCLUDE-Constraints — Postgres prüft beide.

Mit Bedingung: nur aktive Reservierungen

SQL
CREATE TABLE meetings (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    room_id bigint NOT NULL,
    during  tstzrange NOT NULL,
    status  text NOT NULL,
    EXCLUDE USING gist (room_id WITH =, during WITH &&)
        WHERE (status = 'confirmed')
);

WHERE (status = 'confirmed') — der Constraint greift nur für bestätigte Meetings. 'cancelled'-Meetings dürfen sich überschneiden.

Preis-Historie ohne Lücken-Konflikt

SQL
CREATE TABLE product_prices (
    id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    product_id   bigint NOT NULL,
    price        numeric(10,2) NOT NULL,
    valid_during tstzrange NOT NULL,
    EXCLUDE USING gist (product_id WITH =, valid_during WITH &&)
);

Pro Produkt darf zu jedem Zeitpunkt nur ein Preis gelten — keine zwei sich überschneidenden Preis-Perioden.

Performance

Exclusion Constraints nutzen einen GiST-Index — bei großen Tabellen merklich langsamer beim Schreiben als ein B-tree-Unique-Index, aber meist akzeptabel.

Wer's testen will:

SQL
EXPLAIN (ANALYZE, BUFFERS)
INSERT INTO room_bookings (room_id, during)
VALUES (1, '[2026-06-01, 2026-06-02)'::tstzrange);

Bei Hot-Path-Inserts (mehrere hundert pro Sekunde) lohnt sich das Messen. Bei normalen Buchungs-Anwendungen kein Thema.

Besonderheiten

EXCLUDE ist Postgres-only — kein SQL-Standard.

Der Constraint-Typ existiert in keinem anderen RDBMS in vergleichbarer Form. Wer cross-DB portabel schreibt, muss die Logik in Application-Code oder Triggern doppelt implementieren — was bekanntlich race-anfällig ist.

btree_gist nicht vergessen für Mixed-Constraints.

Wer room_id = ... AND during && haben will: ohne btree_gist-Extension scheitert der CREATE INDEX. Extension einmalig pro Datenbank: CREATE EXTENSION btree_gist;. Schema-Migrationen daran erinnern.

Range-Inklusivität entscheidet — Standard ist [).

Default-Range-Form ist „untere Grenze inklusiv, obere exklusiv". Heißt: [10:00, 12:00) und [12:00, 14:00) überschneiden sich NICHT. Sehr praktisch — ohne diese Konvention müsste man bei jedem Termin „eine Sekunde abziehen".

EXCLUDE kann mit WHERE-Filter — wie partielle UNIQUE-Indexe.

EXCLUDE USING gist (...) WHERE (status = 'confirmed') macht den Constraint nur für bestimmte Zeilen aktiv. Ähnlich zu Partial Unique Indexes, aber in der EXCLUDE-Syntax direkt eingebaut.

Mehrere EXCLUDE-Constraints pro Tabelle — okay.

Eine Tabelle kann beliebig viele EXCLUDE-Constraints haben (z. B. einer für „Gast nicht doppelt", einer für „Raum nicht doppelt"). Jeder bekommt seinen eigenen GiST-Index.

Alternative: Trigger — aber race-anfällig.

Ohne EXCLUDE müsste man im BEFORE-INSERT-Trigger SELECT … WHERE during && NEW.during prüfen. Zwei parallele Inserts können beide „kein Konflikt" sehen und beide einfügen. EXCLUDE ist atomar; Trigger sind's nicht (außer mit explizitem Locking, das wieder Performance kostet).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Constraints & Schema-Design

Zur Übersicht