Regeln auf Datenbankebene, die die Anwendung nicht umgehen kann
Constraints erzwingen Datenintegrität dort, wo sie am zuverlässigsten durchgesetzt werden kann: direkt in der Datenbank. Dieser Beitrag zeigt CHECK, UNIQUE und NOT NULL mit echter Syntax, erklärt Named Constraints, Migrationen bei bestehenden Daten und warum Regeln auf Datenbankebene auch bei sauberer Validierung im Anwendungscode unverzichtbar bleiben.
Inhaltsverzeichnis
- 1. Warum Constraints trotz Applikationsvalidierung zählen
- 2. NOT NULL im Detail
- 3. UNIQUE Constraints und zusammengesetzte Eindeutigkeit
- 4. CHECK Constraints im Detail
- 5. Named Constraints und Wartbarkeit
- 6. Constraints und Migrationen bei bestehenden Daten
- 7. Fehlerbehandlung: Constraint-Verletzungen auffangen
- 8. Vendor-Unterschiede bei Constraints
- 9. Performance-Auswirkungen von Constraints
- 10. Zusammenfassung
- 11. FAQ
1. Warum Constraints trotz Applikationsvalidierung zählen
Ein Constraint ist eine Regel, die die Datenbank selbst durchsetzt, unabhängig davon, welche Anwendung oder welcher Prozess gerade schreibend zugreift. Viele Entwickler verlassen sich ausschließlich auf Validierung im Anwendungscode und betrachten Datenbank-Constraints als redundant. Dieser Gedanke übersieht, dass eine Datenbank in der Praxis selten nur von einer einzigen Anwendung beschrieben wird. Batch-Jobs, Admin-Skripte, Datenimporte, andere Microservices und manuelle Korrekturen über einen SQL-Client greifen alle direkt auf dieselben Tabellen zu, ohne die Validierungslogik der Hauptanwendung zu durchlaufen.
Ein Constraint auf Datenbankebene wirkt als letzte Verteidigungslinie, die unabhängig vom Zugriffsweg gilt. Selbst bei einer einzigen Anwendung mit lückenloser Validierung schützt ein Constraint vor Bugs in genau dieser Validierungslogik, vor Race Conditions zwischen gleichzeitigen Requests und vor zukünftigen Entwicklern, die eine Validierungsregel versehentlich entfernen oder falsch portieren. Die Kombination aus Applikationsvalidierung für gute Fehlermeldungen und Datenbank-Constraints für garantierte Integrität ist kein Widerspruch, sondern die einzige robuste Strategie.
Die drei wichtigsten Constraint-Typen für die tägliche Praxis sind NOT NULL, UNIQUE und CHECK. Jeder deckt eine andere Klasse von Regeln ab: Pflichtfelder, Eindeutigkeit und beliebige logische Bedingungen. Die folgenden Abschnitte zeigen jeden Typ mit konkreter Syntax und den Fallstricken, die in der Praxis am häufigsten auftreten.
2. NOT NULL im Detail
NOT NULL ist der einfachste und zugleich am häufigsten unterschätzte Constraint. Er verhindert, dass eine Spalte den Wert NULL annimmt, und zwingt damit jeden INSERT und UPDATE, für diese Spalte einen konkreten Wert zu liefern. Ohne NOT NULL akzeptiert eine Spalte standardmäßig NULL, was in nachgelagerten Abfragen zu unerwarteten Ergebnissen führt: NULL-Werte werden bei Aggregatfunktionen wie SUM oder AVG ignoriert, bei Vergleichsoperatoren wie gleich oder ungleich liefern sie weder wahr noch falsch, sondern unbekannt, was Filterbedingungen auf subtile Weise verfälschen kann.
Die Faustregel für NOT NULL lautet: Jede Spalte, die fachlich immer einen Wert haben muss, sollte NOT NULL tragen, auch wenn die Anwendung aktuell immer einen Wert setzt. Diese Regel schützt vor zukünftigen Codepfaden, die versehentlich einen NULL-Wert einfügen, etwa nach einem Refactoring, das ein Pflichtfeld im Formular entfernt, ohne die Datenbankspalte anzupassen. Für optionale Felder, bei denen NULL fachlich "nicht vorhanden" statt "unbekannt" bedeutet, ist NULL dagegen die korrekte Modellierung, kein Konstruktionsfehler.
CREATE TABLE customer (
customer_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
-- optional: not every customer has a middle name
middle_name VARCHAR(100) NULL,
-- optional: filled only after the account is verified
verified_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Adding NOT NULL to an existing column requires a default
-- for all currently NULL rows first
UPDATE customer SET first_name = 'Unknown' WHERE first_name IS NULL;
ALTER TABLE customer MODIFY first_name VARCHAR(100) NOT NULL;
3. UNIQUE Constraints und zusammengesetzte Eindeutigkeit
Ein UNIQUE-Constraint stellt sicher, dass kein Wert, oder keine Kombination von Werten bei mehreren Spalten, mehrfach in einer Tabelle vorkommt. Anders als ein Primary Key erlaubt UNIQUE in den meisten Datenbanksystemen genau einen NULL-Wert je Spalte, oder in PostgreSQL sogar mehrere NULL-Werte, weil NULL nach der Drei-Wert-Logik von SQL nie als gleich zu einem anderen NULL gilt. Ein UNIQUE-Constraint auf email verhindert doppelte Kundenkonten mit derselben E-Mail-Adresse, unabhängig davon, über welchen Weg der Datensatz eingefügt wird.
Zusammengesetzte UNIQUE-Constraints über mehrere Spalten sind ein häufig übersehenes Werkzeug. Ein Constraint auf (tenant_id, sku) stellt sicher, dass eine Artikelnummer innerhalb eines Mandanten eindeutig ist, verschiedene Mandanten aber dieselbe Artikelnummer unabhängig voneinander verwenden dürfen. Diese Art von Eindeutigkeit lässt sich nicht durch zwei getrennte einzelne UNIQUE-Constraints abbilden, sondern erfordert einen einzigen Constraint über beide Spalten zusammen, da die Kombination, nicht jede Spalte für sich, eindeutig sein soll.
CREATE TABLE product (
product_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id INT UNSIGNED NOT NULL,
sku VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
-- composite uniqueness: sku is only unique per tenant
CONSTRAINT uq_product_tenant_sku UNIQUE (tenant_id, sku)
);
-- Single-column unique constraint, added after table creation
ALTER TABLE customer
ADD CONSTRAINT uq_customer_email UNIQUE (email);
4. CHECK Constraints im Detail
Ein CHECK-Constraint deckt jede beliebige logische Bedingung ab, die sich auf eine oder mehrere Spalten derselben Zeile bezieht. Typische Einsatzfälle sind Wertebereiche wie ein positiver Preis, feste Werteliste wie ein Statusfeld, oder Beziehungen zwischen Spalten wie ein Enddatum, das nicht vor dem Startdatum liegen darf. Bis MySQL 8.0.16 wurden CHECK-Constraints syntaktisch akzeptiert, aber stillschweigend ignoriert, ein häufiger Grund für unerwartet ungültige Daten in älteren MySQL-Versionen. Seit MySQL 8.0.16 sowie in praktisch allen anderen relationalen Datenbanken werden CHECK-Constraints korrekt durchgesetzt.
CHECK-Constraints lassen sich über mehrere Spalten hinweg kombinieren, was sie zu einem mächtigen Werkzeug für fachliche Invarianten macht, die sonst nur in Trigger-Logik abgebildet werden könnten. Ein Constraint wie CHECK (end_date IS NULL OR end_date >= start_date) erzwingt eine zeitliche Reihenfolge, ohne dass die Anwendung diese Prüfung bei jedem einzelnen Schreibzugriff wiederholen muss. Wichtig ist, CHECK-Constraints so zu formulieren, dass sie NULL-Werte explizit berücksichtigen, da eine Bedingung mit NULL in SQL weder wahr noch falsch, sondern unbekannt ergibt und der Constraint dann effektiv nicht greift.
CREATE TABLE product (
product_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NULL,
CONSTRAINT chk_price_positive CHECK (price >= 0),
CONSTRAINT chk_status_values
CHECK (status IN ('draft', 'active', 'discontinued')),
-- explicitly handle NULL: an open end date is always valid
CONSTRAINT chk_date_order
CHECK (end_date IS NULL OR end_date >= start_date)
);
5. Named Constraints und Wartbarkeit
Wird ein Constraint ohne expliziten Namen definiert, vergibt die Datenbank automatisch einen generierten Namen, der oft kryptisch ist und sich zwischen Datenbanksystemen unterscheidet. Ein solcher Name erschwert es erheblich, den Constraint später gezielt zu löschen, zu ändern oder in einer Fehlermeldung zu identifizieren. Die Konvention CONSTRAINT constraint_name CHECK/UNIQUE/... sollte deshalb in jeder Tabellendefinition konsequent verwendet werden, mit einem sprechenden Namen, der Tabelle, Spalte und Constraint-Typ erkennen lässt, etwa chk_product_price_positive oder uq_customer_email.
Named Constraints zahlen sich besonders bei Migrationen aus. Ein ALTER TABLE DROP CONSTRAINT braucht den exakten Namen, und ohne konsistente Namensgebung müssen Entwickler diesen Namen erst über eine Abfrage des Systemkatalogs ermitteln, bevor sie eine Änderung vornehmen können. Eine feste Namenskonvention, projektweit dokumentiert und in Code-Reviews durchgesetzt, spart in der Praxis erheblichen Aufwand bei jeder späteren Schema-Änderung.
6. Constraints und Migrationen bei bestehenden Daten
Einen Constraint nachträglich auf eine Tabelle mit bestehenden Daten anzuwenden, scheitert sofort, wenn auch nur eine einzige Zeile die neue Regel verletzt. Vor jedem ALTER TABLE ADD CONSTRAINT müssen die bestehenden Daten deshalb bereinigt werden, entweder durch ein UPDATE, das ungültige Werte korrigiert, oder durch eine gezielte Analyse, welche Zeilen die Regel verletzen, bevor überhaupt entschieden wird, wie sie behandelt werden. Ein SELECT COUNT vor der Migration, das die Verletzungen zählt, verhindert böse Überraschungen bei einem fehlgeschlagenen ALTER TABLE auf einer großen Tabelle.
Bei sehr großen Tabellen kann das Hinzufügen eines CHECK-Constraints in PostgreSQL mit der Option NOT VALID zunächst ohne vollständige Tabellenprüfung erfolgen, der Constraint gilt dann sofort für neue und geänderte Zeilen, während bestehende Zeilen erst mit einem separaten VALIDATE CONSTRAINT-Befehl geprüft werden, der ohne exklusive Sperre auskommt. Diese zweistufige Vorgehensweise reduziert die Downtime bei Migrationen auf Tabellen mit vielen Millionen Zeilen erheblich.
-- PostgreSQL: add the constraint without a full table scan first,
-- validate existing rows in a second, non-blocking step
ALTER TABLE product
ADD CONSTRAINT chk_price_positive CHECK (price >= 0) NOT VALID;
ALTER TABLE product
VALIDATE CONSTRAINT chk_price_positive;
-- Count violations before attempting the constraint on any system
SELECT COUNT(*) AS violations
FROM product
WHERE price < 0;
7. Fehlerbehandlung: Constraint-Verletzungen auffangen
Eine Constraint-Verletzung führt zu einem Fehler, den die Anwendung explizit behandeln muss, statt sie als generischen Serverfehler an den Endnutzer durchzureichen. Jedes Datenbanksystem liefert dabei einen spezifischen Fehlercode: MySQL nutzt den SQLSTATE 23000 für Integritätsverletzungen, PostgreSQL differenziert genauer zwischen 23502 für NOT NULL, 23505 für UNIQUE und 23514 für CHECK-Verletzungen. Diese Codes erlauben es der Anwendung, gezielt zu reagieren, etwa mit einer verständlichen Fehlermeldung wie "Diese E-Mail-Adresse ist bereits registriert" statt eines undurchsichtigen Datenbankfehlers.
Ein häufiges Anti-Pattern ist, vor jedem INSERT zunächst per SELECT zu prüfen, ob ein Wert bereits existiert, um die Constraint-Verletzung präventiv zu vermeiden. Dieses Vorgehen ist anfällig für Race Conditions, weil zwischen SELECT und INSERT ein anderer Prozess denselben Wert einfügen kann. Der robustere Weg ist, den INSERT direkt zu versuchen und die resultierende Constraint-Verletzung im Fehlerfall gezielt abzufangen, entweder im Anwendungscode oder mit datenbankseitigen Konstrukten wie INSERT ... ON CONFLICT in PostgreSQL beziehungsweise INSERT ... ON DUPLICATE KEY UPDATE in MySQL.
-- PostgreSQL: let the UNIQUE constraint do the work, no prior SELECT
INSERT INTO customer (email, first_name, last_name)
VALUES ('anna@example.com', 'Anna', 'Schmidt')
ON CONFLICT (email) DO NOTHING;
-- MySQL: equivalent pattern for the same race-condition-free insert
INSERT INTO customer (email, first_name, last_name)
VALUES ('anna@example.com', 'Anna', 'Schmidt')
ON DUPLICATE KEY UPDATE first_name = VALUES(first_name);
8. Vendor-Unterschiede bei Constraints
Obwohl NOT NULL, UNIQUE und CHECK Teil des SQL-Standards sind, unterscheidet sich das Verhalten im Detail zwischen den Datenbanksystemen. PostgreSQL erlaubt bei UNIQUE-Constraints mehrere NULL-Werte in derselben Spalte, weil jedes NULL als eigenständig unbekannt behandelt wird. MySQL folgt in der Praxis demselben Verhalten für UNIQUE-Indizes, weicht aber bei anderen Details ab, etwa bei der historischen Nichtdurchsetzung von CHECK vor Version 8.0.16, die weiter oben bereits erwähnt wurde.
SQLite prüft CHECK-Constraints standardmäßig, ist bei Datentypen insgesamt jedoch deutlich toleranter als andere Systeme, was CHECK-Constraints dort zu einem noch wichtigeren Werkzeug macht, um Typkorrektheit sicherzustellen, die SQLite selbst nicht strikt erzwingt. Bei projektübergreifender Portabilität empfiehlt es sich, Constraint-Definitionen auf den kleinsten gemeinsamen Nenner des SQL-Standards zu beschränken und vendor-spezifische Erweiterungen wie partielle Indizes für bedingte Eindeutigkeit gezielt und dokumentiert einzusetzen.
| Constraint-Typ | Was er erzwingt | Typischer Fehlercode | Häufiger Fallstrick |
|---|---|---|---|
| NOT NULL | Spalte darf nicht NULL sein | SQLSTATE 23502 | Bestehende NULL-Werte vor ALTER bereinigen |
| UNIQUE | Wert oder Kombination einmalig | SQLSTATE 23505 | Zusammengesetzte Eindeutigkeit vergessen |
| CHECK | Beliebige logische Bedingung | SQLSTATE 23514 | NULL-Fall nicht explizit behandelt |
| PRIMARY KEY | NOT NULL plus UNIQUE kombiniert | SQLSTATE 23000 / 23505 | Nur ein Primary Key pro Tabelle möglich |
| FOREIGN KEY | Referenz muss existieren | SQLSTATE 23503 | Fehlende ON DELETE-Regel |
9. Performance-Auswirkungen von Constraints
Ein Constraint ist keine kostenlose Sicherheit, jede Prüfung kostet Rechenzeit bei jedem INSERT und UPDATE. NOT NULL und CHECK-Constraints, die nur die aktuelle Zeile betrachten, sind dabei sehr günstig, weil sie ohne zusätzlichen Datenzugriff auf andere Zeilen oder Tabellen auskommen. UNIQUE-Constraints sind teurer, weil die Datenbank intern einen Index pflegen muss, um Eindeutigkeit effizient zu prüfen, was zusätzlichen Speicherplatz und Schreibaufwand bei jedem INSERT verursacht.
Foreign-Key-Constraints sind in der Regel die teuersten, weil jede Prüfung einen Lookup in der referenzierten Tabelle erfordert. In der Praxis überwiegt der Nutzen der garantierten Datenintegrität die Kosten fast immer deutlich, insbesondere weil ein fehlender Constraint die Kosten nicht eliminiert, sondern lediglich in Form von Datenreparaturen und Debugging-Aufwand in die Zukunft verschiebt. Nur bei sehr hochfrequenten Bulk-Imports, bei denen die Datenqualität bereits vorab garantiert ist, lohnt sich das temporäre Deaktivieren von Constraints während des Imports mit anschließender erneuter Validierung.
Mironsoft
Datenmodellierung, Schema-Design und Datenbankberatung
Datenqualität, die sich nicht auf Anwendungscode allein verlässt?
Wir prüfen bestehende Schemata auf fehlende Constraints, entwerfen saubere Namenskonventionen und bringen Bestandsdaten sicher auf ein Niveau, das neue Regeln erlaubt, ohne Downtime für euer System.
Constraint-Audit
Systematische Prüfung fehlender NOT NULL, UNIQUE und CHECK Regeln
Datenbereinigung
Sichere Migration von Bestandsdaten vor dem Hinzufügen neuer Regeln
Fehlerbehandlung
Robuste Behandlung von Constraint-Verletzungen im Anwendungscode
10. Zusammenfassung
NOT NULL, UNIQUE und CHECK sind die drei grundlegenden Constraints, mit denen sich Datenintegrität direkt in der Datenbank erzwingen lässt, unabhängig davon, welche Anwendung oder welcher Prozess schreibend zugreift. NOT NULL schützt Pflichtfelder, UNIQUE verhindert doppelte Werte einzeln oder in Kombination, CHECK deckt beliebige logische Bedingungen bis hin zu Beziehungen zwischen mehreren Spalten ab. Named Constraints mit sprechenden Namen erleichtern Wartung und Migrationen erheblich, während die zweistufige NOT VALID plus VALIDATE-Strategie in PostgreSQL Downtime bei großen Tabellen minimiert.
Applikationsvalidierung und Datenbank-Constraints schließen sich nicht aus, sondern ergänzen sich: Die Anwendung liefert verständliche Fehlermeldungen für Endnutzer, die Datenbank garantiert Integrität, die auch bei Bugs, Race Conditions und alternativen Zugriffswegen bestehen bleibt. Wer Constraints konsequent einsetzt, reduziert Debugging-Aufwand und verhindert Dateninkonsistenzen, die sonst erst Monate später auffallen.
Constraints im Detail, das Wichtigste auf einen Blick
NOT NULL
Erzwingt Pflichtfelder unabhängig vom Zugriffsweg, schützt vor stillen NULL-Bugs.
UNIQUE
Verhindert Duplikate einzeln oder als Spaltenkombination, robuster als eine SELECT-Prüfung.
CHECK
Erzwingt beliebige logische Bedingungen, NULL-Fall muss explizit behandelt werden.
Migration
Bestandsdaten bereinigen, dann NOT VALID plus VALIDATE für große Tabellen ohne Downtime nutzen.