Unique Constraints in MariaDB - Gleichzeitige Requests in der Datenbank konkurrieren lassen
Zwei Registrierungsanfragen fordern nahezu gleichzeitig denselben Benutzernamen an. Jede Anfrage prüft die Datenbank, und jede sieht, dass der Name anscheinend verfügbar ist. Wenn anschließend beide eine Zeile einfügen dürfen, welche Schicht stellt dann sicher, dass die Datenbank am Ende nur einen Eigentümer enthält?
Eine Verfügbarkeitsprüfung auf Anwendungsebene kann frühzeitig eine hilfreiche Meldung liefern, beobachtet aber nur einen einzelnen Zeitpunkt. Eine andere Anfrage kann nach dieser Prüfung und vor dem Einfügen schreiben. Ein UNIQUE-Schlüssel verlagert die endgültige Entscheidung dorthin, wo beide Schreibvorgänge zusammentreffen müssen: in die Datenbank. Das nützliche Versprechen ist eng begrenzt. MariaDB kann einen gespeicherten Wert oder eine Kombination ablehnen, die im Unique Index bereits vorhanden ist; die Datenbank kann nicht entscheiden, ob die Geschäftsregel richtig modelliert wurde.
Dieser Artikel konzentriert sich auf diese Grenze in einer kleinen PHP-Anwendung mit MariaDB. Er ersetzt kein allgemeines Transaktionsdesign und behauptet nicht, dass die Anfrage gewinnen muss, die zuerst am Webserver eintrifft.
Mit der Ein-Eigentümer-Regel beginnen
Ein Unique Key sollte eine Invariante ausdrücken und nicht lediglich eine Abfrage beschleunigen. Beispiele sind „ein Konto pro Benutzername“, „ein Webhook-Eingang pro Provider und Delivery-ID“ oder „ein Kategorie-Slug innerhalb jeder Website“. Jeder Satz benennt sowohl den Wert als auch seinen Geltungsbereich.
Für einen global eindeutigen Benutzernamen kann die Tabelle einen benannten Schlüssel deklarieren:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(80) NOT NULL,
display_name VARCHAR(120) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uq_users_username UNIQUE (username)
) ENGINE=InnoDB;
Der Primary Key identifiziert jede Zeile. Der zusätzliche Unique Key schützt eine andere Regel: Zwei Zeilen dürfen keine Benutzernamen speichern, die unter der Collation der Spalte als gleich gelten. Ein benannter Schlüssel macht Schema-Prüfungen und Diagnosen verständlicher, auch wenn das Anwendungsverhalten nicht vom Parsen der menschenlesbaren Fehlermeldung abhängen sollte.
Wenn derselbe Benutzername in unterschiedlichen Websites oder Tenants vorkommen darf, wäre globale Eindeutigkeit zu streng. Ein zusammengesetzter Schlüssel kann den kleineren Geltungsbereich ausdrücken:
CONSTRAINT uq_users_site_username UNIQUE (site_id, username)
Dadurch wird weder site_id noch username für sich genommen eindeutig. Eindeutig wird die vollständige Kombination. Dieser Unterschied entscheidet häufig darüber, ob ein Constraint die Domäne beschreibt oder gültige Daten ablehnt.
Die Verfügbarkeitsprüfung bleibt nützlich, hat aber nicht das letzte Wort
Ein Formular kann vor dem Absenden prüfen, ob ein Benutzername verfügbar erscheint. Das verbessert das Feedback und kann einen offensichtlich aussichtslosen Versuch vermeiden. Allein durch das Lesen wird der Wert jedoch nicht reserviert.
Betrachten wir zwei unabhängige Anfragen:
- Anfrage A sucht nach
riverund findet keine Zeile. - Anfrage B sucht ebenfalls nach
riverund findet keine Zeile. - Beide versuchen, eine Zeile einzufügen.
- MariaDB akzeptiert einen gespeicherten Wert und lehnt den kollidierenden Schreibvorgang über den Unique Key ab.
Die genaue Ausführungsreihenfolge ist ein Detail von Datenbank und Workload, keine der Anwendung zugesicherte faire Warteschlange. Der Constraint liefert den Endzustand: Beide kollidierenden Werte werden nicht als committed Zeilen gespeichert. Die Anwendung sollte „verfügbar“ daher als vorläufig betrachten, bis der Schreibvorgang erfolgreich war.
Der Versuch, die Lücke allein durch eine umfangreichere Anwendungsprüfung zu schließen, kann zusätzliche Locking- und Transaktionskomplexität erzeugen und dennoch einen anderen Writer übersehen, der nicht demselben Codepfad folgt. Ein Schema-Constraint gilt für PHP-Handler, Importe, Wartungsskripte und zukünftige Anwendungen, die dieselbe Tabelle verwenden.
Definieren, was „derselbe Wert“ bedeutet
Das Wort Duplikat klingt eindeutig, bis Strings und fehlende Werte beteiligt sind. Die Datenbank wendet konkrete Vergleichsregeln an, und diese Regeln müssen zur Invariante passen.
NULL ist kein reservierter leerer Platz
Die MariaDB-Dokumentation zu CREATE TABLE erklärt, dass ein Unique Key bei einer nullable Spalte mehrere Zeilen mit NULL zulässt. Wenn jedes Konto einen eindeutigen Benutzernamen haben muss, sollte UNIQUE deshalb mit NOT NULL kombiniert werden. Andernfalls können mehrere Zeilen wahrheitsgemäß ausdrücken, dass kein Benutzername vorhanden ist.
Dieses Verhalten kann sinnvoll sein, wenn ein optionaler Bezeichner fehlt, sollte aber eine bewusste Modellentscheidung sein. Leere Strings, ausschließlich aus Whitespace bestehende Strings und NULL sind außerdem unterschiedliche Eingaben. Die Anwendungsvalidierung kann einige davon ablehnen oder normalisieren, bevor die Datenbank die Eindeutigkeit bewertet.
Die Collation bestimmt die Gleichheit mit
Bei Zeichenspalten bedeutet Eindeutigkeit nicht zwingend Byte-für-Byte-Identität. Die MariaDB-Referenz zu Fehler 1062 erklärt, dass die Collation der Spalte bestimmt, ob Werte Duplikate sind. Unter einer passenden, nicht zwischen Groß- und Kleinschreibung unterscheidenden Collation gelten River und river als gleich. Auch Akzentsensitivität und die Behandlung nachfolgender Leerzeichen können das Ergebnis beeinflussen.
Für einen Login-Namen kann das genau richtig und für einen anderen Bezeichner falsch sein. Eine Collation sollte nicht lediglich geändert werden, um einen Duplicate-Fehler zu beseitigen. Zuerst ist zu entscheiden, welche Unterschiede die Domäne bewahren soll; anschließend sollten Zeichensatz, Collation, Validierung und Normalisierungsregeln gemeinsam ausgewählt und getestet werden. Veränderte Vergleichsregeln können bei einer bestehenden Tabelle Kollisionen sichtbar machen, die zuvor erlaubt waren.
Den Insert entscheiden lassen und den erwarteten Konflikt übersetzen
MariaDB meldet einen doppelten Unique oder Primary Key als Fehler 1062 mit SQLSTATE 23000. Laut PHP-Handbuch ist der Exception-Modus von PDO seit PHP 8.0 der Standard. Ein kleiner Handler kann den Insert versuchen und nur den bekannten Duplicate-Fall übersetzen:
<?php
$insert = $pdo->prepare(
'INSERT INTO users (username, display_name)
VALUES (:username, :display_name)'
);
try {
$insert->execute([
'username' => $validatedUsername,
'display_name' => $validatedDisplayName,
]);
} catch (PDOException $error) {
$sqlState = (string) $error->getCode();
$driverCode = (int) ($error->errorInfo[1] ?? 0);
if ($sqlState === '23000' && $driverCode === 1062) {
throw new DomainException('That username is no longer available.');
}
throw $error;
}
Die doppelte Prüfung ist wichtig. PDO standardisiert SQLSTATE, aber 23000 bezeichnet eine Klasse von Integritätsverletzungen und keinen ausschließlich für Duplikate geltenden Code. Die Dokumentation zu PDOException stellt treiberspezifische Informationen über errorInfo bereit; MariaDBs enger Duplicate-Code lautet 1062. Das Abgleichen des Datenbanktexts ist fragiler und kann einen Wert oder Indexnamen preisgeben, wenn die rohe Meldung in eine öffentliche Response gelangt.
Das Beispiel dient der Veranschaulichung. Ein realer Endpoint benötigt weiterhin Eingabevalidierung, Autorisierung, gegebenenfalls CSRF-Schutz, sicheres Fehler-Logging sowie eine angemessene HTTP- oder Formular-Response. Wenn mehrere Unique Keys fehlschlagen können, reicht Fehler 1062 möglicherweise nicht aus, um das kollidierende Domänenfeld zu bestimmen. Die Anwendung benötigt eventuell eine begrenzte Zuordnung bekannter Key-Metadaten, eine nachfolgende Abfrage unter Beachtung der Offenlegungsregeln oder eine generische Konfliktmeldung. Einem nicht authentifizierten Besucher sollte nicht mitgeteilt werden, dass eine sensible E-Mail-Adresse bereits ein Konto besitzt, sofern diese Offenlegung nicht beabsichtigt ist.
Duplicate-Semantik wählen, statt den Fehler zu verstecken
Ein Duplikat bedeutet nicht immer, dass der Geschäftsprozess fehlgeschlagen ist. MariaDB dokumentiert mehrere Alternativen, die jedoch unterschiedliche Bedeutungen haben:
- Einfaches
INSERT: Der kollidierende Schreibvorgang wird abgelehnt. Das ist ein klarer Standard, wenn der Benutzer einen anderen Wert wählen muss. INSERT IGNORE: Eine doppelte Zeile wird übersprungen und als Warnung gemeldet. Dies kann zu einer bewusst entworfenen Bulk-Import-Regel passen, kann einen Aufrufer aber auch glauben lassen, die Arbeit sei erfolgt, obwohl eine Zeile ausgelassen wurde.INSERT ... ON DUPLICATE KEY UPDATE: Ein Duplikat wird in ein Update umgewandelt. Das ist nur sinnvoll, wenn die bestehende Zeile das beabsichtigte Update-Ziel ist und der Aufrufer sie ändern darf.REPLACE: Die kollidierende Zeile wird gelöscht und eine neue eingefügt. Die Delete-und-Insert-Semantik kann Bezeichner, Foreign Keys und Trigger betreffen; das vertraute Wort „replace“ darf daher nicht mit einem gewöhnlichen Update verwechselt werden.
Die offizielle Dokumentation zu ON DUPLICATE KEY UPDATE enthält eine wichtige Warnung: Treffen mehrere Unique Indexes zu, wird nur der erste aktualisiert, und MariaDB empfiehlt diese Anweisung nicht für Tabellen mit mehr als einem Unique Index. Ein Upsert ist somit eine Geschäftsregel und kein universelles Mittel, um Exception Handling zu vermeiden.
Die Regel vorsichtig zu bestehenden Daten hinzufügen
Eine neue Tabelle beginnt leer; eine bestehende Tabelle kann der vorgeschlagenen Invariante bereits widersprechen. Sie sollte mit denselben Spaltendefinitionen und derselben Collation geprüft werden, die der Schlüssel verwenden wird. Für eine nullable E-Mail-Spalte könnte eine erste Diagnoseabfrage so aussehen:
SELECT email, COUNT(*) AS row_count
FROM users
WHERE email IS NOT NULL
GROUP BY email
HAVING COUNT(*) > 1;
Eine solche Abfrage sollte nur in einem angemessen geschützten administrativen Kontext ausgeführt werden, weil ihr Ergebnis Kontodaten enthält. Ein nicht leeres Ergebnis ist keine Erlaubnis, Zeilen willkürlich zu löschen. Die Duplikate können zusammenzuführende Konten, historische Importe, Schreibweisen mit unterschiedlicher Groß- und Kleinschreibung oder eine Regel darstellen, die die Anwendung zuvor nie zugesichert hat. Vor der Schemaänderung muss entschieden werden, wie Datensätze zusammengeführt, umgeleitet, umbenannt oder geprüft werden.
Wenn Datenmodell und Datensätze übereinstimmen, kann die Migration den Schlüssel hinzufügen:
ALTER TABLE users
ADD CONSTRAINT uq_users_email UNIQUE (email);
Die MariaDB-Dokumentation zu ALTER TABLE erklärt, dass dieser Vorgang bei vorhandenen Duplikaten mit einem Fehler abbricht. Sie dokumentiert außerdem ALTER ... IGNORE, das Duplikate verwerfen kann, ohne kontrollieren zu können, welche Zeilen erhalten bleiben. Wenn Zeilen eine Bedeutung haben, ist dies kein geeigneter Bereinigungsmechanismus.
Die Migration ist als betriebliche Änderung zu behandeln. ALTER TABLE kann auf Metadata Locks warten, und verfügbare Algorithmen und Lock-Level hängen von MariaDB-Version, Storage Engine und konkreter Operation ab. Die installierte Version sollte geprüft, mit repräsentativen Daten getestet, ein Wiederherstellungsplan bereitgehalten und die resultierende Definition mit SHOW CREATE TABLE verifiziert werden. Aus dem kurzen SQL-Beispiel lässt sich keine universelle „Online“-Aussage ableiten.
Was der Unique Key nicht garantiert
Ein Unique Key ist stark, weil seine Aussage begrenzt ist. Er:
- autorisiert den Aufrufer nicht, einen Wert zu beanspruchen oder zu aktualisieren;
- beweist nicht, dass ein Benutzername, eine E-Mail-Adresse oder ein externer Bezeichner echt ist;
- normalisiert Unicode, Whitespace oder anwendungsspezifische Aliase nicht selbstständig;
- macht mehrere zusammengehörige Anweisungen nicht atomar;
- koordiniert keine unabhängigen Datenbanken oder entfernten Dienste;
- garantiert nicht, dass die von Menschen als „zuerst“ wahrgenommene Anfrage von MariaDB akzeptiert wird.
Transaktionen bleiben erforderlich, wenn ein Vorgang mehrere Zeilen gemeinsam ändert. Foreign Keys schützen Beziehungen. Validierung erklärt fehlerhafte Eingaben. Autorisierung entscheidet, wer handeln darf. Ein Unique Key liefert eine getrennte Garantie: Nach Maßgabe seiner Spalten und Vergleichsregeln werden kollidierende gespeicherte Schlüssel abgelehnt.
Checkliste für das Review
- Die Invariante einschließlich ihres Geltungsbereichs in einem Satz formulieren.
- Einen einspaltigen oder zusammengesetzten Schlüssel wählen, der zu diesem Geltungsbereich passt.
- Entscheiden, ob fehlende Werte gültig sind;
NOT NULLhinzufügen, wenn ein Wert erforderlich ist. - Groß- und Kleinschreibung, Akzente, Whitespace und Normalisierung unter der tatsächlich verwendeten Collation testen.
- Verfügbarkeitsprüfungen als vorläufig behandeln und den Schreibvorgang dem Constraint aussetzen.
- SQLSTATE und den MariaDB-Treibercode behandeln, ohne rohe Datenbankmeldungen zurückzugeben.
- Reject-, Ignore-, Update- oder Replacement-Semantik aus dem Geschäftsprozess ableiten.
- Bestehende Duplikate prüfen und vor der Migration bewusst auflösen.
- Gleichzeitige Versuche testen und die endgültige Zeilenzahl prüfen, nicht nur die Response einer Anfrage.
Fazit
Das Rennen zwischen Prüfung und Insert lässt sich nicht dadurch lösen, dass dieselbe Frage mit größerer Zuversicht an die Datenbank gestellt wird. Die Verfügbarkeitsabfrage kann einem Menschen helfen, während der Unique Key die gespeicherte Invariante schützt, wenn gleichzeitige Writer zusammentreffen. Einer der Schreibvorgänge kann dann einen normalen, erwarteten Konflikt erhalten, den PHP in die Sprache der Domäne übersetzt.
Die schwierige Aufgabe besteht nicht darin, UNIQUE zu schreiben. Sie besteht darin, Geltungsbereich, Nullability und Vergleichsregeln richtig festzulegen, die Bedeutung eines Duplikats für den jeweiligen Vorgang zu bestimmen und ältere Daten abzugleichen, ohne sie stillschweigend zu zerstören. Sind diese Entscheidungen ausdrücklich getroffen, kann der Constraint eine kleine, aber wichtige Aufgabe zuverlässig erfüllen: einen zweiten Eigentümer für einen Schlüssel abzulehnen, der laut Schema nur einer Zeile gehören darf.
References
- MariaDB Documentation.
CREATE TABLE. Abgerufen am 30. Oktober 2026. - MariaDB Documentation. Error 1062: Duplicate entry for key. Abgerufen am 30. Oktober 2026.
- MariaDB Documentation.
INSERT ... ON DUPLICATE KEY UPDATE. Abgerufen am 30. Oktober 2026. - MariaDB Documentation.
ALTER TABLE. Abgerufen am 30. Oktober 2026. - PHP Documentation Group. PDO: Errors and error handling. Abgerufen am 30. Oktober 2026.
- PHP Documentation Group.
PDOException. Abgerufen am 30. Oktober 2026. - PostgreSQL Global Development Group. PostgreSQL 18 Documentation: Unique Constraints. Abgerufen am 30. Oktober 2026.
