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

TypElement-TypUse-Case
int4rangeintegerID-Bereiche, Mengen-Spannen
int8rangebigintwie int4range, aber für bigint
numrangenumericPreisspannen, exakte Zahlen-Bereiche
daterangedateDatum-Spannen ohne Uhrzeit
tsrangetimestampZeitspannen ohne TZ (selten sinnvoll)
tstzrangetimestamptzZeitspannen — 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:

SchreibweiseBedeutung
[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
SQL Beispiele
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:

SQL Konferenzraum-Buchungen
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

OperatorBedeutungBeispiel
&&Überschneidungperiod && period2
@>enthält Wert oder Rangeperiod @> now()
<@ist enthalten in'2026-05-07'::date <@ daterange
=gleichperiod = period2
<< / >>strikt links / rechts vonperiod << '[2026-06-01, …)'

In der Praxis:

SQL Buchungen abfragen
-- 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.

SQL EXCLUDE USING gist
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:

SQL
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

SQL Auf Bestandteile zugreifen
-- 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;
SQL Ranges kombinieren
-- 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:

SQL
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

/ Weiter

Zurück zu Datentypen

Zur Übersicht