Daten in MySQL validieren
Ein praxisnaher Leitfaden zur Datenqualität in MySQL — die sechs Prüfungen, die jede Tabelle braucht, das SQL dazu und die Fallen aus impliziter Typumwandlung, Kollation und Null-Datumswerten, die MySQL so gut darin machen, schlechte Daten zu verstecken.
· 7 min read
Um Daten in MySQL zu validieren, formulieren Sie jede Erwartung als Aggregatabfrage, die eine Anzahl von Verstößen zurückgibt, führen sie als Stapel aus und lassen sie fehlschlagen, sobald ein Zähler seine Schwelle überschreitet. Die sechs Prüfungen, auf die es ankommt, sind: Nullwerte in Pflichtspalten, doppelte Schlüssel, Werte außerhalb einer erlaubten Menge, Zahlen außerhalb eines plausiblen Bereichs, fehlerhaft formatierte Zeichenketten und veraltete Zeilen. MySQL macht das schwerer, als es aussieht, denn es ist die Engine, die schlechte Daten am bereitwilligsten stillschweigend annimmt: Implizite Typumwandlung, nachsichtige Datumswerte und kollationsabhängige Gleichheit sorgen gemeinsam dafür, dass ungültige Zeilen gültig aussehen.
Warum MySQL schlechte Daten versteckt
Drei Verhaltensweisen erklären die meisten Überraschungen.
Implizite Typumwandlung. Eine Zeichenkettenspalte mit einer Zahl zu vergleichen wirft keinen Fehler — MySQL castet die Zeichenkette. WHERE amount_text = 0 trifft auf 'abc', '' und '0.00' gleichermaßen zu, weil alle drei zu 0 umgewandelt werden. Jede Validierung, die über Typgrenzen hinweg vergleicht, misst etwas anderes, als Sie denken.
Null-Datumswerte. Solange NO_ZERO_DATE und NO_ZERO_IN_DATE nicht im sql_mode stehen, ist '0000-00-00' ein speicherbares DATE. Es ist nicht NULL, Vollständigkeitsprüfungen bestehen also; es ist kein echtes Datum, Aktualitätsprüfungen vergleichen also dagegen und melden die Zeile als uralt.
Kollationsabhängige Gleichheit. Die Standardkollation utf8mb4_0900_ai_ci ignoriert Akzente und Groß-/Kleinschreibung, 'PAID', 'paid' und 'páid' sind also ein Wert. Auf einer _bin- oder _cs-Spalte sind es drei. Dieselbe Prüfung auf erlaubte Werte bedeutet auf zwei Tabellen derselben Datenbank Unterschiedliches.
Nichts davon ist ein Fehler. Es sind Voreinstellungen, um die herum Sie validieren müssen. Die Prüfungen selbst sind dieselben sechs, die Sie gegen jede Engine schreiben würden — der vollständige Leitfaden zur Datenvalidierung behandelt diesen gemeinsamen Ablauf; was folgt, ist die MySQL-korrekte Fassung jeder einzelnen.
Die sechs Prüfungen, die jede Tabelle braucht
1. Vollständigkeit — Nullwerte in Pflichtspalten
SELECT COUNT(*) AS violations
FROM `sales`.`orders`
WHERE `order_id` IS NULL;
Auf einer Tabelle, die älter ist als der Strict Mode, erweitern Sie das um das Null-Äquivalent der leeren Zeichenkette:
WHERE `order_id` IS NULL OR `order_id` = '';
2. Eindeutigkeit — doppelte Geschäftsschlüssel
SELECT COUNT(*) AS violations
FROM (
SELECT `order_id`
FROM `sales`.`orders`
WHERE `order_id` IS NOT NULL
GROUP BY `order_id`
HAVING COUNT(*) > 1
) AS dupes;
Denken Sie an den Kollationsvorbehalt: Auf einer Spalte ohne Beachtung der Groß-/Kleinschreibung meldet diese Prüfung 'ORD-1' und 'ord-1' korrekt als Duplikate. Auf einer _bin-Spalte nicht. Entscheiden Sie, was Sie meinen, und schreiben Sie die Kollation fest.
3. Konformität — Werte außerhalb einer erlaubten Menge
SELECT COUNT(*) AS violations
FROM `sales`.`orders`
WHERE `status` IS NOT NULL
AND `status` NOT IN ('pending', 'paid', 'shipped', 'refunded');
Die IS NOT NULL-Absicherung ist Pflicht — NULL NOT IN (...) ergibt NULL, die Zeile fällt heraus, und eine überwiegend mit Nullwerten gefüllte Spalte meldet null Verstöße.
Der ENUM-Typ von MySQL sieht so aus, als mache er diese Prüfung überflüssig. Tut er nicht: Außerhalb des Strict Mode wird ein ungültiger ENUM-Wert als leere Zeichenkette gespeichert statt abgewiesen — die Spalte kann also einen Wert enthalten, der in ihrer eigenen Definition gar nicht vorkommt.
4. Genauigkeit — Zahlen außerhalb eines plausiblen Bereichs
SELECT COUNT(*) AS violations
FROM `sales`.`orders`
WHERE `total_amount` IS NOT NULL
AND (`total_amount` < 0 OR `total_amount` > 100000);
Nutzen Sie DECIMAL für Geldbeträge. FLOAT und DOUBLE vergleichen mit Rundungsfehlern, und eine vorzeichenlose Integer-Spalte lässt einen negativen Wert still zu einem sehr großen positiven überlaufen — was eine Bereichsprüfung erwischt und ein Schema-Constraint nicht.
5. Konformität — fehlerhaft formatierte Identifier
SELECT COUNT(*) AS violations
FROM `sales`.`orders`
WHERE `reference` IS NOT NULL
AND `reference` NOT REGEXP '^ORD-[0-9]{6}#39;;
MySQL 8.0 hat die alte POSIX-Engine durch ICU ersetzt, \d, \w, genügsame Quantoren und Unicode-Klassen funktionieren also alle. Auf MySQL 5.7 oder MariaDB ist die ältere Engine strenger — bleiben Sie bei POSIX-sicheren Mustern wie [0-9] und [[:alpha:]], wenn Sie beides unterstützen müssen. REGEXP ist außerdem kollationsabhängig: Bei einer _ci-Kollation ist der Abgleich unabhängig vom Muster nicht groß-/kleinschreibungssensitiv — nutzen Sie also REGEXP BINARY, wenn die Groß-/Kleinschreibung zählt.
6. Aktualität — Freshness
SELECT COUNT(*) AS violations
FROM `sales`.`orders`
WHERE `created_at` < UTC_TIMESTAMP() - INTERVAL 24 HOUR;
Nutzen Sie UTC_TIMESTAMP(), nicht NOW(). NOW() liefert die Zeitzone der Sitzung, die bei einem Client aus einer anderen Region nicht die des Servers ist — und die Prüfung verschiebt sich still. Ist die Spalte ein TIMESTAMP statt eines DATETIME, wandelt MySQL beim Schreiben bereits nach UTC um, eine der wenigen Stellen, an denen sein implizites Verhalten hilft.
Ergänzen Sie eine Absicherung gegen Null-Datumswerte, wo der Strict Mode nicht erzwungen wird:
WHERE `created_at` = '0000-00-00 00:00:00'
OR `created_at` < UTC_TIMESTAMP() - INTERVAL 24 HOUR;
Einen sicheren Nutzer mit Lesezugriff einrichten
CREATE USER 'catalyst_ro'@'%' IDENTIFIED BY '<generated>';
GRANT SELECT ON `analytics`.* TO 'catalyst_ro'@'%';
SELECT allein genügt — information_schema ist für jeden Account lesbar und automatisch auf die Objekte beschränkt, die dieser Account sehen darf; der Schema-Import braucht also kein zusätzliches Recht. Wenn Sie eine Replik betreiben, richten Sie die Verbindung dorthin: Das sind aggregierende Scans, und sie gehören von der schreibenden Primärinstanz weg.
Setzen Sie eine Obergrenze, damit ein Scan auf einer nicht indizierten Tabelle keinen Thread blockieren kann:
SET SESSION max_execution_time = 60000; -- milliseconds, SELECT only
Von Ad-hoc-SQL zum Data Contract
Handgeschriebene Prüfungen verfallen. Die verlässliche Variante ist deklarativ: Die Erwartungen leben neben dem Schema, in einem Format, das sich in einem Pull Request prüfen lässt und von einem Runner ausgeführt werden kann. Catalyst nutzt den Open Data Contract Standard:
apiVersion: v3.0.0
kind: DataContract
info:
title: orders
version: 1.1.0
owner: data-platform
schema:
- name: orders
physicalName: orders
physicalType: table
properties:
- name: order_id
logicalType: string
physicalType: char(36)
required: true
primaryKey: true
quality:
- rule: nullCount
dimension: completeness
severity: error
mustBe: "0"
- rule: duplicateCount
dimension: uniqueness
severity: error
mustBe: "0"
- name: status
logicalType: string
quality:
- rule: validValues
dimension: conformity
severity: error
mustBe: "['pending', 'paid', 'shipped', 'refunded']"
- name: total_amount
logicalType: number
physicalType: decimal(12,2)
quality:
- rule: between
dimension: accuracy
severity: error
mustBe: "[0, 100000]"
- name: reference
logicalType: string
quality:
- rule: regex
dimension: conformity
severity: warning
mustBe: "'^ORD-[0-9]{6}#39;"
- name: created_at
logicalType: timestamp
quality:
- rule: freshness
dimension: timeliness
severity: error
mustBe: "<= 24h"
Catalyst importiert Spalten und Typen aus information_schema, schlägt einen Basis-Contract vor, kompiliert jede Regel zu dem MySQL-SQL von oben und hält pro Prüfung pass, warn oder fail fest — samt der betroffenen Zeilen zur Einsicht. Das YAML läuft verlustfrei hin und zurück, eine im Builder bearbeitete Regel formatiert die Datei also nicht um und verwirft Ihre Kommentare nicht.
MySQL-Stolperfallen, die man kennen sollte
| Falle | Was passiert | Was zu tun ist |
|---|---|---|
| Implizite Typumwandlung | 'abc' = 0 ist wahr | Nie über Typgrenzen hinweg vergleichen |
| Null-Datumswerte | '0000-00-00' besteht Null-Prüfungen | NO_ZERO_DATE aktivieren; explizit absichern |
ENUM außerhalb des Strict Mode | Ungültiger Wert wird als '' gespeichert | Trotzdem eine validValues-Regel führen |
_ci-Kollation | 'PAID' ist gleich 'paid' | Kollation festschreiben oder REGEXP BINARY |
NOW() | Zeitzone der Sitzung | UTC_TIMESTAMP() verwenden |
| Vorzeichenlose Integer | Negativ läuft zu riesig positiv über | Eine between-Regel ergänzen |
Geldbeträge als FLOAT | Bereichsgrenzen durch Rundung verschoben | DECIMAL verwenden |
| Regex in MySQL 5.7 | POSIX-Engine, kein \d | [0-9] nutzen oder auf 8.0 aktualisieren |
Zeitplanung und Alerting
Lassen Sie die Prüfungen eines Datasets direkt nach dem Job laufen, der es lädt, und richten Sie den Zeitplan nach dem Takt der Daten statt nach einem runden Cron-Ausdruck. Alarmieren Sie auf Übergänge — eine Prüfung, die von pass auf fail wechselt — statt bei jedem Lauf einer weiterhin fehlschlagenden Prüfung, und behalten Sie den Schweregrad error den Regeln vor, die nachgelagerte Konsumenten tatsächlich stoppen sollten. Musterprüfungen auf Freitextfeldern gehören meist auf warning, wo sie ein Trend sind und kein Weckruf.
Häufige Fragen
Kann ich MySQL-Daten validieren, ohne SQL zu schreiben?
Ja. Deklarieren Sie die Erwartung — Pflichtfeld, eindeutig, erlaubte Werte, numerischer Bereich, Regex, Höchstalter, Fremdschlüssel — und das Tool kompiliert sie zu MySQL-korrektem SQL. Catalyst liest Ihre Spalten aus information_schema, schlägt aus den gefundenen Typen und Nullable-Angaben einen Ausgangs-Contract vor und verlangt SQL nur dort, wo die Logik wirklich maßgeschneidert ist — über eine customSql-Regel.
Braucht Datenvalidierung Schreibzugriff auf MySQL?
Nein. GRANT SELECT genügt — jede Prüfung ist ein aggregierendes SELECT, das eine einzelne Zahl zurückgibt. Catalyst verbindet sich read-only und speichert nur Metadaten und Ergebnisse; Ihre Zeilen bleiben in Ihrer Datenbank.
Funktioniert das mit MariaDB und Amazon Aurora MySQL?
Ja, mit einem Vorbehalt: MariaDB hat die ältere POSIX-Regex-Engine behalten, Muster mit \d, \w oder genügsamen Quantoren verhalten sich dort also anders als auf MySQL 8.0. Bleiben Sie für portable Regeln bei POSIX-Zeichenklassen. Aurora MySQL verhält sich für alles in diesem Leitfaden wie das Upstream-MySQL.
Warum besteht meine Vollständigkeitsprüfung auf einer Spalte voller leerer Zeichenketten?
Weil '' nicht NULL ist. Außerhalb des Strict Mode wandelt MySQL viele ungültige Inserts in leere Zeichenketten oder Nullwerte um, statt sie abzuweisen — eine Spalte kann also vollständig gefüllt und dabei völlig bedeutungslos sein. Schreiben Sie die Vollständigkeitsprüfung so, dass sie beides abdeckt, und ergänzen Sie eine validValues- oder regex-Regel für das, was die Spalte tatsächlich enthalten soll.
Wie verhält sich das zu CHECK-Constraints?
MySQL setzt CHECK-Constraints erst seit 8.0.16 durch — davor wurden sie geparst und ignoriert, was eine eigene Kategorie von Falle ist. Selbst dort, wo sie durchgesetzt werden, weisen Constraints Zeilen beim Schreiben ab, und das ist für eine Analysetabelle oft das falsche Verhalten: Häufig will man die Daten lieber landen lassen und in Quarantäne stellen. Ein Data Contract beschreibt die Zusage, läuft nach Zeitplan und liefert Ihnen die fehlerhaften Zeilen statt eines abgewiesenen Ladelaufs.