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