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.
Inhaltsverzeichnis
- 1. Was ein Check Constraint über Wertebereiche hinaus leisten kann
- 2. Mehrspalten-Check-Constraints für Feldbeziehungen
- 3. Zustandsautomaten mit Check Constraints abbilden
- 4. Check Constraints mit eigenen Funktionen kombinieren
- 5. Bereichsüberlappungen verhindern mit Exclude Constraints
- 6. Grenzen von Check Constraints: was nicht geht
- 7. Named Constraints, Fehlermeldungen und Nutzererfahrung
- 8. Teststrategie für komplexe Check Constraints
- 9. Check Constraint vs. Trigger vs. Anwendungscode
- 10. Zusammenfassung
- 11. FAQ
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.