Check Constraints für komplexe Geschäftsregeln
AI generated
SELECT
JOIN
SQL · Constraints · Geschäftsregeln
Check Constraints für komplexe Geschäftsregeln
von einfachen Wertebereichen zu Zustandsautomaten und Überlappungsschutz

Ein CHECK Constraint kann weit mehr als einen positiven Preis erzwingen. Mehrspalten-Bedingungen, Zustandsautomaten für Statusfelder, IMMUTABLE-Funktionen und EXCLUDE Constraints gegen überlappende Zeiträume machen aus einer simplen Wertebereichsprüfung ein Werkzeug für komplexe Geschäftsregeln, die sonst nur mit Triggern oder Anwendungscode abgebildet würden.

18 Min. Lesezeit CHECK · EXCLUDE · Zustandsautomat PostgreSQL · Standard-SQL

1. Was ein Check Constraint über Wertebereiche hinaus leisten kann

Die meisten Entwickler kennen einen Check Constraint nur als einfache Wertebereichsprüfung, etwa dass ein Preis nicht negativ sein darf. Diese Sicht unterschätzt erheblich, was ein CHECK Constraint tatsächlich leisten kann. Der SQL-Standard erlaubt in der CHECK-Klausel jeden booleschen Ausdruck, der sich auf die Spalten derselben Zeile bezieht, einschließlich beliebig verschachtelter logischer Verknüpfungen, CASE-Ausdrücken und, in PostgreSQL, sogar Aufrufen benutzerdefinierter Funktionen.

Diese Mächtigkeit erlaubt es, komplexe Geschäftsregeln direkt im Schema zu verankern, statt sie ausschließlich im Anwendungscode zu prüfen. Ein Zustandsautomat für ein Bestellstatusfeld, eine Regel, die mehrere Datumsfelder in eine logische Reihenfolge bringt, oder eine Bedingung, die abhängig von einem Diskriminator-Feld unterschiedliche Pflichtfelder verlangt, lassen sich alle als ein einziger, deklarativer Check Constraint formulieren. Der entscheidende Vorteil gegenüber einem Trigger: ein CHECK Constraint ist deklarativ, wird im Schema selbst sichtbar dokumentiert und lässt sich vom Query-Planer für Optimierungen nutzen.

Die folgenden Abschnitte zeigen konkrete Muster für komplexe Geschäftsregeln, die weit über die übliche einfache Wertebereichsprüfung hinausgehen, zusammen mit den Grenzen, an denen ein Check Constraint an einen Trigger oder Anwendungscode übergeben werden muss.

2. Mehrspalten-Check-Constraints für Feldbeziehungen

Ein Mehrspalten-Check Constraint bezieht mehrere Felder derselben Zeile in eine gemeinsame Bedingung ein. Ein klassisches Beispiel ist ein Zeitraum, bei dem das Enddatum nicht vor dem Startdatum liegen darf, oder eine Preisregel, bei der ein reduzierter Preis niemals höher sein darf als der reguläre Preis. Solche Regeln betreffen die Beziehung zwischen Spalten, nicht den Wert einer einzelnen Spalte für sich, und sind deshalb mit einem einzelnen CHECK pro Spalte nicht ausdrückbar.

Wichtig bei Mehrspalten-Constraints ist die konsequente Behandlung von NULL-Werten, da eine Bedingung, die auf einen NULL-Wert trifft, in der Drei-Wert-Logik von SQL weder wahr noch falsch, sondern unbekannt ergibt, und der Constraint dann effektiv nicht greift. Ein Enddatum, das optional ist, muss deshalb explizit mit einer OR-Bedingung für den NULL-Fall abgesichert werden, sonst wird jede Zeile mit NULL im Enddatum unbeabsichtigt akzeptiert, ganz gleich, welchen Wert das Startdatum trägt.


CREATE TABLE promotion (
    promotion_id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name           VARCHAR(255) NOT NULL,
    regular_price  DECIMAL(10,2) NOT NULL,
    sale_price     DECIMAL(10,2) NOT NULL,
    start_date     DATE NOT NULL,
    end_date       DATE,
    CONSTRAINT chk_sale_price_lower
        CHECK (sale_price <= regular_price),
    -- explicit NULL handling: an open end date is always valid
    CONSTRAINT chk_promotion_date_order
        CHECK (end_date IS NULL OR end_date >= start_date),
    -- three-column relationship in a single declarative rule
    CONSTRAINT chk_discount_percentage_realistic
        CHECK (
            (regular_price - sale_price) / NULLIF(regular_price, 0) <= 0.90
        )
);

3. Zustandsautomaten mit Check Constraints abbilden

Ein Statusfeld wie ein Bestellstatus durchläuft in der Praxis eine feste Menge fachlich sinnvoller Werte, und häufig sind bestimmte Kombinationen mit anderen Feldern nur in bestimmten Status zulässig. Ein Check Constraint, der über eine simple IN-Liste hinausgeht, kann diese Beziehung direkt abbilden: eine Bestellung im Status "cancelled" muss ein cancelled_at-Datum tragen, eine Bestellung im Status "shipped" muss eine Tracking-Nummer haben, eine Bestellung im Status "draft" darf keines von beiden haben.

Diese Art von Zustandsautomat lässt sich als eine CASE-Bedingung innerhalb des CHECK Constraints formulieren, die für jeden möglichen Status prüft, welche Begleitfelder gesetzt oder NULL sein müssen. Der Vorteil gegenüber einer Prüfung im Anwendungscode: die Regel gilt garantiert für jeden Schreibzugriff, auch für Admin-Skripte oder direkte SQL-Updates, die die Anwendungslogik umgehen. Ein Übergang, der versehentlich einen inkonsistenten Zustand erzeugt, etwa ein "shipped"-Status ohne Tracking-Nummer, wird von der Datenbank selbst abgelehnt.


CREATE TABLE customer_order (
    order_id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    status          VARCHAR(20) NOT NULL,
    cancelled_at    TIMESTAMP,
    tracking_number VARCHAR(64),
    CONSTRAINT chk_order_status_values
        CHECK (status IN ('draft', 'confirmed', 'shipped', 'cancelled')),
    -- state machine: each status enforces its own required companion fields
    CONSTRAINT chk_order_status_consistency
        CHECK (
            (status = 'cancelled' AND cancelled_at IS NOT NULL AND tracking_number IS NULL)
            OR (status = 'shipped' AND tracking_number IS NOT NULL AND cancelled_at IS NULL)
            OR (status IN ('draft', 'confirmed') AND cancelled_at IS NULL AND tracking_number IS NULL)
        )
);

4. Check Constraints mit eigenen Funktionen kombinieren

PostgreSQL erlaubt es, in einem Check Constraint eine benutzerdefinierte Funktion aufzurufen, solange diese als IMMUTABLE markiert ist. Das bedeutet, die Funktion muss für dieselben Eingabewerte garantiert immer dasselbe Ergebnis liefern, ohne von externem Zustand, der aktuellen Zeit oder anderen Tabellen abzuhängen. Diese Einschränkung existiert, weil der Query-Planer und die Indexpflege sich auf deterministisches Verhalten verlassen müssen.

Eine IMMUTABLE-Funktion eignet sich hervorragend, um wiederkehrende, komplexe Validierungslogik wie ein Regex-Muster für eine IBAN-Prüfziffer oder eine benutzerdefinierte Prüfsumme in einer einzigen Funktion zu kapseln und diese Funktion dann in mehreren CHECK Constraints über verschiedene Tabellen hinweg wiederzuverwenden. Das vermeidet Code-Duplikation zwischen Tabellen, die dieselbe fachliche Regel prüfen müssen, etwa eine gültige ISO-Ländercode-Prüfung in mehreren Adress-Tabellen.


-- PostgreSQL: reusable IMMUTABLE function for a business rule check
CREATE OR REPLACE FUNCTION is_valid_iso_country_code(code TEXT)
RETURNS BOOLEAN AS $$
    SELECT code ~ '^[A-Z]{2}$' AND code IN (
        'DE', 'AT', 'CH', 'FR', 'IT', 'ES', 'NL', 'BE', 'PL', 'US'
        -- full list truncated for brevity
    );
$$ LANGUAGE sql IMMUTABLE;

CREATE TABLE shipping_address (
    address_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    country_code  CHAR(2) NOT NULL,
    CONSTRAINT chk_address_country_valid
        CHECK (is_valid_iso_country_code(country_code))
);

CREATE TABLE billing_address (
    address_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    country_code  CHAR(2) NOT NULL,
    -- same business rule reused across a different table
    CONSTRAINT chk_billing_country_valid
        CHECK (is_valid_iso_country_code(country_code))
);

5. Bereichsüberlappungen verhindern mit Exclude Constraints

Manche Geschäftsregeln lassen sich nicht als einfacher CHECK Constraint ausdrücken, weil sie mehrere Zeilen derselben Tabelle miteinander vergleichen müssen, etwa dass sich zwei Buchungen für denselben Raum zeitlich nicht überlappen dürfen. PostgreSQL löst dieses Problem mit dem EXCLUDE Constraint, der eng verwandt mit einem Check Constraint ist, aber auf Zeilenvergleiche statt auf eine einzelne Zeile wirkt. Mit dem Erweiterungsmodul btree_gist und einem Bereichstyp wie tstzrange lässt sich eine Überlappungsregel deklarativ und indexgestützt durchsetzen.

Der entscheidende Vorteil eines EXCLUDE Constraints gegenüber einer Anwendungslogik, die vor jedem INSERT manuell auf Überlappungen prüft: die Prüfung ist race-condition-frei, weil sie innerhalb derselben Transaktion und desselben Locking-Mechanismus wie der Schreibvorgang selbst erfolgt. Zwei gleichzeitige Buchungsversuche für denselben Zeitraum können damit nicht beide erfolgreich sein, selbst unter hoher Parallelität.


-- PostgreSQL: prevent overlapping bookings for the same room
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_booking (
    booking_id  BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    room_id     INT NOT NULL,
    during      TSTZRANGE NOT NULL,
    -- no two bookings for the same room may overlap in time
    CONSTRAINT excl_room_booking_overlap
        EXCLUDE USING gist (room_id WITH =, during WITH &&)
);

-- This insert succeeds
INSERT INTO room_booking (room_id, during)
VALUES (101, tstzrange('2026-08-01 09:00', '2026-08-01 10:00'));

-- This insert fails: overlaps with the row above for the same room
INSERT INTO room_booking (room_id, during)
VALUES (101, tstzrange('2026-08-01 09:30', '2026-08-01 10:30'));

6. Grenzen von Check Constraints: was nicht geht

Ein Check Constraint darf sich im Standard-SQL ausschließlich auf Spalten derselben Zeile beziehen, niemals auf andere Zeilen derselben Tabelle oder auf andere Tabellen. Eine Regel wie "diese Bestellung darf nur existieren, wenn der referenzierte Kunde aktiv ist" lässt sich deshalb nicht als CHECK Constraint formulieren, sondern erfordert entweder einen Foreign Key kombiniert mit einem Trigger, oder eine Prüfung im Anwendungscode. PostgreSQL erlaubt zwar technisch Subqueries in bestimmten Kontexten nicht, MySQL und die meisten anderen Systeme untersagen Subqueries in CHECK Constraints explizit.

Eine zweite wichtige Grenze betrifft nicht-deterministische Ausdrücke: ein CHECK Constraint mit CURRENT_TIMESTAMP oder RANDOM() ist in vielen Datenbanksystemen entweder verboten oder wird nur beim Schreibzeitpunkt einmalig ausgewertet, nicht kontinuierlich neu geprüft. Eine Regel wie "das Startdatum darf nicht in der Vergangenheit liegen" gilt deshalb nur zum Zeitpunkt des Schreibens, nicht dauerhaft, was für die meisten Anwendungsfälle korrekt, aber ein häufiges Missverständnis ist.

7. Named Constraints, Fehlermeldungen und Nutzererfahrung

Bei komplexen Geschäftsregeln ist ein sprechender Name für den Check Constraint noch wichtiger als bei einfachen Wertebereichsprüfungen, weil der Constraint mehrere fachliche Bedingungen gleichzeitig kodiert. Ein Name wie chk_order_status_consistency verrät sofort, welche Regel verletzt wurde, während ein generischer, automatisch generierter Name in der Fehlermeldung keinen Hinweis auf die eigentliche fachliche Ursache gibt.

Da die Fehlermeldung eines verletzten CHECK Constraints in der Regel den Constraint-Namen enthält, kann die Anwendung diesen Namen parsen und in eine verständliche, benutzerfreundliche Meldung übersetzen. Eine Mapping-Tabelle im Anwendungscode, die Constraint-Namen auf lokalisierte Fehlermeldungen abbildet, verhindert, dass Endnutzer eine kryptische Datenbank-Fehlermeldung zu sehen bekommen, während die eigentliche Durchsetzung der Regel weiterhin zuverlässig in der Datenbank liegt.

8. Teststrategie für komplexe Check Constraints

Ein komplexer Check Constraint mit mehreren Bedingungen und CASE-Verzweigungen verdient dieselbe Testdisziplin wie eine Funktion im Anwendungscode. Ein pragmatischer Ansatz ist eine Reihe von Testfällen, die jede gültige und jede ungültige Kombination gezielt einfügt und erwartet, dass der Insert entweder erfolgreich ist oder mit dem erwarteten Constraint-Namen fehlschlägt. Diese Tests laufen idealerweise in einer Transaktion, die am Ende zurückgerollt wird, damit die Testdaten die Datenbank nicht dauerhaft verschmutzen.

Für Zustandsautomaten mit vielen Kombinationen empfiehlt sich eine tabellengetriebene Teststruktur, die systematisch jede Statusübergang-Kombination gegen die erwartete Gültigkeit prüft. Dieser systematische Test deckt Randfälle auf, die bei rein manuellem Testen leicht übersehen werden, etwa eine vergessene Kombination im CASE-Ausdruck, die versehentlich als gültig durchgeht.


-- Table-driven test for the order state machine constraint,
-- rolled back at the end so no test data persists
BEGIN;

-- Valid: cancelled with cancelled_at, no tracking number
INSERT INTO customer_order (status, cancelled_at, tracking_number)
VALUES ('cancelled', now(), NULL);

-- Invalid: shipped without a tracking number, expected to fail
DO $$
BEGIN
    INSERT INTO customer_order (status, cancelled_at, tracking_number)
    VALUES ('shipped', NULL, NULL);
    RAISE EXCEPTION 'Expected constraint violation did not occur';
EXCEPTION
    WHEN check_violation THEN
        RAISE NOTICE 'Constraint correctly rejected invalid shipped row';
END $$;

ROLLBACK;

9. Check Constraint vs. Trigger vs. Anwendungscode

Die Wahl zwischen einem Check Constraint, einem Trigger und einer Prüfung im Anwendungscode hängt davon ab, ob die Regel sich auf eine einzelne Zeile beschränkt, mehrere Zeilen oder Tabellen einbezieht, und ob sie deterministisch ausgewertet werden kann.

Regeltyp Empfohlenes Werkzeug Begründung
Wertebereich, eine Spalte Check Constraint Einfach, deklarativ, keine zusätzliche Logik nötig
Zustandsautomat, mehrere Spalten Check Constraint CASE-Ausdruck deckt Feldkombinationen deklarativ ab
Überlappungsschutz mehrerer Zeilen Exclude Constraint Race-condition-frei, indexgestützt, zeilenübergreifend
Prüfung gegen andere Tabelle Trigger Check Constraint darf keine anderen Tabellen lesen
Externe API-Validierung Anwendungscode Datenbank darf keine externen Aufrufe ausführen

Mironsoft

Datenmodellierung, Schema-Design und Datenbankberatung

Geschäftsregeln, die im Schema garantiert gelten?

Wir modellieren Zustandsautomaten, Überlappungsschutz und mehrspaltige Regeln direkt als Constraints und entlasten euren Anwendungscode von Validierungslogik, die sonst dupliziert und unvollständig bleibt.

Regel-Modellierung

Zustandsautomaten und Feldbeziehungen als deklarative Constraints

Überlappungsschutz

Exclude Constraints für Buchungen, Zeiträume und Ressourcen

Test-Suite

Tabellengetriebene Constraint-Tests für alle Zustandsübergänge

10. Zusammenfassung

Ein Check Constraint ist weit mehr als eine Prüfung für positive Preise. Mehrspalten-Bedingungen, Zustandsautomaten für Statusfelder, IMMUTABLE-Funktionen für wiederverwendbare Validierungslogik und EXCLUDE Constraints gegen Bereichsüberlappungen erlauben es, komplexe Geschäftsregeln direkt und deklarativ im Schema zu verankern. Der Vorteil gegenüber Anwendungscode: die Regel gilt garantiert unabhängig vom Zugriffsweg, ist race-condition-frei und wird im Schema selbst dokumentiert.

Die Grenzen liegen dort, wo ein Check Constraint auf andere Tabellen zugreifen oder externe Zustände einbeziehen müsste, hier übernehmen Trigger oder Anwendungscode. Named Constraints mit sprechenden Namen, eine systematische Teststrategie und das bewusste Abwägen zwischen Constraint, Trigger und Anwendungscode machen komplexe Geschäftsregeln nachvollziehbar, statt sie über mehrere Ebenen zu verstreuen.

Check Constraints für komplexe Geschäftsregeln, das Wichtigste auf einen Blick

Mehrspalten-Bedingungen

Feldbeziehungen wie Zeitraum- und Preisregeln in einem einzigen deklarativen Constraint bündeln.

Zustandsautomaten

CASE-Ausdrücke in CHECK erzwingen konsistente Feldkombinationen je Status.

Exclude Constraints

Überlappungsschutz für Buchungen und Zeiträume, race-condition-frei und indexgestützt.

Grenzen kennen

Kein Zugriff auf andere Tabellen, nicht-deterministische Ausdrücke vermeiden.

11. FAQ: Check Constraints für komplexe Geschäftsregeln

1Kann ein Check Constraint mehrere Spalten prüfen?
Ja, jeder boolesche Ausdruck über beliebig viele Spalten derselben Zeile ist erlaubt, etwa eine Beziehung zwischen Start- und Enddatum.
2Wie bilde ich einen Zustandsautomaten ab?
Mit einer CASE-Bedingung im CHECK, die je Status prüft, welche Begleitfelder gesetzt oder NULL sein müssen.
3Was bedeutet IMMUTABLE bei Funktionen?
Die Funktion liefert für dieselben Eingaben garantiert dasselbe Ergebnis, ohne von externem Zustand abzuhängen.
4Wie verhindere ich überlappende Buchungen?
Mit einem EXCLUDE Constraint über einen Bereichstyp und btree_gist, race-condition-frei im Gegensatz zu manuellen Prüfungen.
5Kann ein Check Constraint andere Tabellen lesen?
Nein, im Standard-SQL nur Spalten derselben Zeile. Für tabellenübergreifende Regeln braucht es einen Trigger.
6Warum scheitert CURRENT_TIMESTAMP im Check?
Der Ausdruck ist nicht-deterministisch und wird nur zum Schreibzeitpunkt geprüft, nicht dauerhaft neu bewertet.
7Wie teste ich komplexe Constraints systematisch?
Mit tabellengetriebenen Tests, die jede Kombination gezielt einfügen und in einer rückrollbaren Transaktion prüfen.
8Wie mache ich Fehlermeldungen benutzerfreundlich?
Mit sprechenden Constraint-Namen und einer Mapping-Tabelle im Anwendungscode für lokalisierte Meldungen.
9Wann Trigger statt Check Constraint?
Wenn die Regel andere Tabellen einbezieht oder ein Seiteneffekt wie ein Audit-Log ausgelöst werden muss.
10Lohnt sich der Aufwand für komplexe Constraints?
Ja, die Regel gilt garantiert unabhängig vom Zugriffsweg und ist im Schema selbst dokumentiert, statt verstreut im Code.