Range-Typen sind eine elegante Postgres-Spezialität: ein Wertebereich (z. B. „Datum von—bis”, „Preisspanne 100—200 €”) als ein einziger Spaltenwert. Mit eingebauten Operatoren für Überschneidungen und einer Killer-Funktion: Exclusion Constraints, die automatisch Doppel-Buchungen verhindern.
Eingebaute Range-Typen
| Typ | Element-Typ | Use-Case |
|---|---|---|
int4range | integer | ID-Bereiche, Mengen-Spannen |
int8range | bigint | wie int4range, aber für bigint |
numrange | numeric | Preisspannen, exakte Zahlen-Bereiche |
daterange | date | Datum-Spannen ohne Uhrzeit |
tsrange | timestamp | Zeitspannen ohne TZ (selten sinnvoll) |
tstzrange | timestamptz | Zeitspannen — die Standard-Wahl |
Du kannst auch eigene Range-Typen über CREATE TYPE … AS RANGE definieren — selten nötig, weil die Built-ins fast alles abdecken.
Range-Literale schreiben
Ranges haben Klammern für die Grenzen-Inklusivität:
| Schreibweise | Bedeutung |
|---|---|
[a, b] | inklusiv beide Grenzen — a <= x <= b |
(a, b) | exklusiv beide Grenzen — a < x < b |
[a, b) | inklusiv unten, exklusiv oben — a <= x < b |
(a, b] | exklusiv unten, inklusiv oben — a < x <= b |
SELECT '[1, 10]'::int4range; -- 1 bis 10 inklusive
SELECT '[1, 10)'::int4range; -- 1 bis 9 (10 ausgeschlossen)
SELECT '[2026-05-07, 2026-05-14)'::daterange;
SELECT '[2026-05-07 14:00, 2026-05-07 16:00)'::tstzrange;Konvention: für Zeit- und Datumsbereiche fast immer [start, end) verwenden — also Anfang inklusiv, Ende exklusiv. Damit haben benachbarte Ranges (z. B. „09:00–10:00” und „10:00–11:00”) keine Überschneidung am Übergang.
int4range und int8range werden bei diskreten Element-Typen automatisch normalisiert: '[1, 10]' und '[1, 11)' werden gleich gespeichert.
Tabelle mit tstzrange — Buchungs-System
Klassischer Use-Case:
CREATE TABLE bookings (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id integer NOT NULL,
period tstzrange NOT NULL,
booked_by text NOT NULL
);
INSERT INTO bookings (room_id, period, booked_by) VALUES
(1, '[2026-05-07 09:00, 2026-05-07 10:00)', 'alice'),
(1, '[2026-05-07 10:00, 2026-05-07 11:30)', 'bob'),
(1, '[2026-05-07 14:00, 2026-05-07 16:00)', 'carol');Range-Operatoren
| Operator | Bedeutung | Beispiel |
|---|---|---|
&& | Überschneidung | period && period2 |
@> | enthält Wert oder Range | period @> now() |
<@ | ist enthalten in | '2026-05-07'::date <@ daterange |
= | gleich | period = period2 |
<< / >> | strikt links / rechts von | period << '[2026-06-01, …)' |
In der Praxis:
-- Welche Buchungen überschneiden sich mit '14:30 bis 15:30'?
SELECT * FROM bookings
WHERE room_id = 1
AND period && tstzrange('2026-05-07 14:30+02', '2026-05-07 15:30+02');
-- Welche Buchung läuft gerade jetzt?
SELECT * FROM bookings
WHERE period @> now();
-- Buchungen für einen bestimmten Tag
SELECT * FROM bookings
WHERE period && tstzrange('2026-05-07 00:00+02', '2026-05-08 00:00+02');Exclusion Constraints — automatische Doppel-Buchung-Verhinderung
Die wirkliche Killer-Anwendung von Ranges: ein Constraint, der garantiert, dass für denselben Raum keine sich überschneidenden Buchungen existieren.
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE bookings
ADD CONSTRAINT no_overlap
EXCLUDE USING gist (
room_id WITH =,
period WITH &&
);Das liest sich: „Es darf keine zwei Zeilen geben, in denen room_id gleich ist (=) und period sich überschneidet (&&).”
btree_gist ist eine Extension, die normale Equality-Vergleiche in GIST-Indexen erlaubt — sonst kann man room_id (ein int) nicht zusammen mit period (ein Range) in einem GIST verwenden.
In Aktion:
myapp=> INSERT INTO bookings (room_id, period, booked_by) VALUES
(1, '[2026-05-07 09:30, 2026-05-07 10:30)', 'dave');
ERROR: conflicting key value violates exclusion constraint "no_overlap"
DETAIL: Key (room_id, period)=(1, [2026-05-07 09:30+02,2026-05-07 10:30+02))
conflicts with existing key (room_id, period)=(1, [2026-05-07 09:00+02,2026-05-07 10:00+02)).Postgres lehnt den Insert ab, weil Alice schon den Raum von 09:00 bis 10:00 hat — das überschneidet sich mit Daves Wunsch von 09:30 bis 10:30. Ohne Constraint hätte die App das selbst prüfen müssen, mit allen Race-Condition-Risiken bei nebenläufigen Buchungen. Der DB-Constraint macht das atomar.
Range-Funktionen
-- Untere und obere Grenze
SELECT lower(period), upper(period) FROM bookings;
-- Inklusivität prüfen
SELECT lower_inc(period), upper_inc(period) FROM bookings;
-- Ist der Range leer? (untere = obere bei exklusiv)
SELECT isempty('[5, 5)'::int4range); -- t
-- Länge eines Datums-Range in Tagen
SELECT upper(period)::date - lower(period)::date AS days
FROM bookings;-- Vereinigung (geht nur, wenn benachbart oder überlappend)
SELECT '[1, 5)'::int4range + '[5, 10)'::int4range;
-- [1, 10)
-- Schnittmenge
SELECT '[1, 8)'::int4range * '[5, 12)'::int4range;
-- [5, 8)
-- Differenz
SELECT '[1, 10)'::int4range - '[5, 8)'::int4range;
-- ERROR: Differenz nicht eindeutig (würde zwei Ranges ergeben)Multiranges (PG 14+)
Ein einzelner Range kann nur einen zusammenhängenden Bereich darstellen. Für „Liste mehrerer Bereiche” gibt es seit PG 14 die Multirange-Typen:
SELECT int4multirange(
int4range(1, 5),
int4range(10, 15),
int4range(20, 25)
);
-- {[1,5),[10,15),[20,25)}Praktisch für „Verfügbarkeits-Slots” oder „mehrere Urlaubs-Wochen”. Multiranges haben dieselben Operatoren wie Ranges plus Funktionen zum Iterieren über Sub-Ranges.
Besonderheiten
[start, end) ist die idiomatische Konvention.
Inklusiv unten, exklusiv oben. Damit haben benachbarte Ranges (z. B. Stundenslots 09–10 und 10–11) keine Überschneidung am Übergang. In den Postgres-Built-ins ist das auch das Default-Verhalten beim impliziten Konstruktor tstzrange(a, b).
Exclusion Constraints sind atomar — anders als App-Checks.
Ein „erst SELECT, dann INSERT”-Pattern in der Anwendung hat Race Conditions: zwei parallele Sessions können beide den SELECT erfolgreich machen, dann beide INSERTs durchführen — Doppel-Buchung. Mit EXCLUDE USING gist ist das in der DB selbst geregelt; einer der parallelen Inserts schlägt fehl.
GIST-Index braucht btree_gist für gemischte Spalten.
EXCLUDE USING gist (room_id WITH =, period WITH &&) braucht CREATE EXTENSION btree_gist. Ohne diese Extension kann gist keine Equality-Vergleiche auf normalen Typen wie integer. Die Extension ist im Core-Distribution enthalten — nur ein CREATE EXTENSION-Schritt nötig.
Open-ended Ranges mit -infinity / infinity.
'[2026-01-01, infinity)'::tstzrange für „ab 1.1.2026 ohne Ende-Datum”. Praktisch für Verträge oder Subscriptions ohne Ablauf. '(-infinity, 2026-01-01)' für „bis 1.1.2026”. lower_inf(r) und upper_inf(r) prüfen auf Unendlichkeit.
Range-Indexe: GIST oder SP-GIST.
Für Range-Spalten ist CREATE INDEX … USING gist (range_col) die richtige Wahl — beschleunigt &&-, @>- und <@-Queries. SP-GIST kann in manchen Fällen schneller sein, ist aber seltener nötig. B-tree funktioniert NICHT für Range-Operatoren.
Ranges in Anwendungs-APIs serialisieren.
Treiber haben unterschiedlichen Umgang: node-postgres liefert das Postgres-Range-Literal als String; psycopg2 hat einen Range-Type. Bei JSON-APIs typisch: zwei separate Felder (start, end) im JSON, in der DB als Range zusammengeführt. Konvertierung über simple Mapper-Funktionen.
Weiterführende Ressourcen
Externe Quellen
- Range Types – PostgreSQL Documentation
- Range Functions and Operators
- EXCLUDE Constraint – CREATE TABLE
- btree_gist Extension
- Release Notes 14: Multiranges