Irgendwann schaltet jemand die Prüfung ab. Vorher ist meist dasselbe passiert: Ein fehlendes Länderkürzel hat um drei Uhr nachts den kompletten Load gestoppt, am Morgen fehlten dem Fachbereich die Zahlen, und die Ursache war eine Nebensächlichkeit. Binäres pass/fail lässt einem nur die Wahl, ob eine Regel alles blockiert oder gar nichts. Wer Datenqualität stattdessen über drei Schweregrade steuert, trennt „darf nicht weiter“ von „sollte sich jemand ansehen“ — und trifft mit derselben Einstufung noch eine zweite Entscheidung: ob eine Regel zusätzlich als hartes Constraint ins Ziel-Schema gehört oder allein in der Pipeline bleibt.
Das Wichtigste vorab:
- Binäres pass/fail scheitert an beiden Enden. Entweder blockiert eine Kleinigkeit den Load, oder alle Befunde laufen ungelesen ins Log. Beides endet mit einer abgeschalteten Prüfung.
- Drei Schweregrade decken die Praxis ab: Information dokumentiert, Warnung macht sichtbar, Error blockiert.
- Der Schweregrad ist ein Routing-Entscheid. In diesem Modell kann nur eine Error-Regel im Ziel-Schema ein Gegenstück als
CHECK– oderUNIQUE-Constraint bekommen — automatisch entsteht es nicht. Warnung und Information bleiben immer in der Pipeline. - Vier Orte der Durchsetzung — Quelle, Pipeline, Ziel-Schema, Reporting — mit dem, was jeder kann und was er kostet.
- Hochstufen hat einen Preis: Aus einer Beobachtung wird eine Garantie, und der Bestand muss dafür sauber sein.
NOT VALIDundVALIDATE CONSTRAINTzeigen das im Zeitraffer. - Die ehrliche Grenze: Ein
CHECKdarf mehrere Spalten derselben Zeile vergleichen, aber nicht über Zeilen, Tabellen oder die Zeit hinweg prüfen. Dafür ist die Pipeline der richtige Ort — ein Trigger erzwingt es nur im begründeten Ausnahmefall.
Voraussetzung: Postgres als Beispiel-Engine und das Framework aus dem Sub-Hub Datenqualität mit SQL prüfen mit gemeinsamer Fehlertabelle, Regel-Config und Quality Gate. Die Entscheidungslogik gilt darüber hinaus für jedes Prüfsystem, das Befunde sammelt.
Inhalt
- Warum binär scheitert
- Drei Schweregrade für die Datenqualität
- Der Schweregrad ist ein Routing-Entscheid
- Vier Orte, eine Entscheidung
- Warum das Vorsieb trotzdem vorab prüft
- Was beim Hochstufen passiert
- Die Grenzen, ehrlich
- Entscheidungsmatrix
- FAQ
- Verwandte Artikel
Warum binär scheitert
Ein Datenqualitäts-System mit nur einem Schalter kennt zwei Betriebsarten, und beide führen auf demselben Weg zum Abschalten.
Steht der Schalter auf „streng“, blockiert jede verletzte Regel den Load. Das klingt nach Disziplin und funktioniert genau bis zur ersten Nebensächlichkeit. Ein Kunde ohne E-Mail-Adresse ist kein Grund, dem Fachbereich am Morgen die Umsatzzahlen vorzuenthalten. Nach dem dritten nächtlichen Einsatz wegen eines Bagatell-Befunds entsteht Druck, und der endet erfahrungsgemäß nicht in einer besseren Regel, sondern im Kommentarzeichen vor der Prüfung.
Steht der Schalter auf „locker“, läuft alles durch, und die Befunde landen in einem Report. Der wird in der ersten Woche gelesen, in der zweiten überflogen und ab der dritten ungelesen gelöscht. Eine Prüfung ohne Konsequenz ist Dokumentation, keine Kontrolle.
Der Reflex, so ein System abzuschalten, ist verständlich und kein Zeichen mangelnder Disziplin. Das Problem liegt im Modell. Ein einzelnes bestanden/durchgefallen kann den Unterschied zwischen „dieser Datensatz ist unbrauchbar“ und „hier fehlt eine Telefonnummer“ nicht transportieren. Genau diesen Unterschied braucht die Steuerung aber.
Drei Schweregrade für die Datenqualität
Die Lösung ist unspektakulär und bewährt: drei Stufen statt einem Bit. Schweregrad-Modelle sind so alt wie Syslog und so verbreitet wie Linter-Level, und genau deshalb funktionieren sie: Jeder versteht sie ohne Schulung.
- Information (
I) dokumentiert. Der Befund wird protokolliert und taucht in keiner Blockade-Logik auf. Typisch ist ein selten gepflegtes Zusatzfeld, dessen Lücken man kennen will, ohne je deswegen einzugreifen. - Warnung (
W) macht sichtbar. Der Befund soll jemandem auffallen, aber er hält keinen Load an. Erfahrungsgemäß gehören die meisten realen Datenprobleme in diese Stufe. - Error (
E) blockiert. Der Datensatz würde am Ziel scheitern oder fachlich Schaden anrichten. Er bleibt im Staging liegen, bis jemand die Ursache klärt.
Der Schweregrad hängt dabei an der Regel, nicht am einzelnen Befund. Jede Zeile der Regel-Config trägt ihre Einstufung selbst, und jeder Befund erbt sie. In der Config-Tabelle des Frameworks ist das eine Spalte mit einem CHECK-Guard, der die Config selbst sauber hält:
1: CREATE TABLE dq.check_rule
2: (
3: id bigint NOT NULL GENERATED ALWAYS AS IDENTITY
4: ,check_type text NOT NULL
5: ,schema_name text NOT NULL
6: ,table_name text NOT NULL
7: ,id1_column text NOT NULL
8: ,check_column text NOT NULL
9: ,where_clause text
10: ,severity char(1) NOT NULL DEFAULT 'E'
11: ,message text NOT NULL
12: ,active boolean NOT NULL DEFAULT true
13: ,CONSTRAINT pk_check_rule PRIMARY KEY (id)
14: ,CONSTRAINT ck_check_rule_severity CHECK (severity IN ('E', 'W', 'I'))
15: );
Der Default in Zeile 10 ist bewusst E. Wer eine Regel schreibt, meint sie zunächst ernst. Das Herabstufen auf Warnung ist eine bewusste Entscheidung, und die trifft man leichter als das nachträgliche Verschärfen. Was beim Verschärfen alles mitkommt, zeigt der Abschnitt zum Hochstufen. So sehen drei Beispiel-Regeln aus, eine je Stufe:
1: INSERT INTO dq.check_rule
2: (check_type, schema_name, table_name, id1_column, check_column, where_clause, severity, message)
3: VALUES
4: ('constraint', 'staging', 'customer', 'customer_id', 'age', 'age < 0 OR age > 120', 'E', 'Alter ausserhalb 0..120')
5: ,('constraint', 'staging', 'customer', 'customer_id', 'email', 'email IS NULL', 'W', 'E-Mail fehlt')
6: ,('constraint', 'staging', 'customer', 'customer_id', 'phone', 'phone IS NULL', 'I', 'Telefonnummer nicht erfasst');
Das Quality Gate wertet danach ausschließlich die Error-Zähler aus. So sieht der Demo-Bestand aus, bevor der Runner läuft — vier Kunden-Zeilen, von sauber bis doppelt auffällig:
customer_id | email | phone | age | Befund |
|---|---|---|---|---|
| 1 | a@example.com | +49 30 111111 | 30 | sauber |
| 2 | b@example.com | NULL | 200 | Alter unmöglich (E), Telefon fehlt (I) |
| 3 | NULL | +43 1 222222 | 45 | E-Mail fehlt (W) |
| 4 | NULL | NULL | 25 | E-Mail fehlt (W), Telefon fehlt (I) |
Drei dieser vier Zeilen passieren das Gate:
1: SELECT
2: customer_id
3: ,sys_error
4: ,sys_warning
5: ,sys_info
6: FROM
7: staging.customer
8: WHERE
9: sys_error = 0
10: ORDER BY
11: customer_id;
| customer_id | sys_error | sys_warning | sys_info |
|---|---|---|---|
| 1 | 0 | 0 | 0 |
| 3 | 0 | 1 | 0 |
| 4 | 0 | 1 | 1 |
Kunde 2 bleibt liegen: Das unmögliche Alter ist der einzige Error im Bestand. Die Kunden 3 und 4 fließen weiter, obwohl die E-Mail fehlt. Ihre Warnungen sind protokolliert, blockieren aber nicht. Und das sys_info = 1 bei Kunde 4 ist die fehlende Telefonnummer — erfasst, ohne dass deswegen je jemand eingreifen muss. Niemand musste dafür nachts aufstehen.
Der Schweregrad ist ein Routing-Entscheid
Bis hierher wäre der Schweregrad nur ein besseres Etikett. Interessant wird er durch eine zweite Rolle, die auf den ersten Blick nichts mit Blockieren zu tun hat: Er entscheidet, ob eine Regel allein in der Pipeline durchgesetzt wird oder zusätzlich im Ziel-Schema.
Das Muster stammt aus einem Metadaten-getriebenen ETL-Projekt, und dort ist die Reihenfolge der eigentliche Punkt: Am Anfang steht nicht die Regel, sondern das restriktive Datenmodell. Die Metadaten legen fest, welche Spalten Pflicht sind, welche Schlüssel eindeutig sein müssen und welche Wertebereiche gelten. Daraus entsteht das Ziel-Schema mit seinen harten Constraints, und aus denselben Metadaten werden die Error-Regeln der Regel-Config abgeleitet: Jede harte Garantie des Ziel-Schemas bekommt automatisch ihre Vorab-Prüfung mit Kritikalität E. Eine Warnung entsteht auf diesem Weg nie. Sie wird von Hand ergänzt, weil sie etwas beobachten soll, das das Datenmodell bewusst nicht erzwingt. Die Kritikalität ist in diesem Modell kein Report-Feld. Sie zeigt an, ob hinter einer Regel eine Schema-Garantie steht.
Die Logik dahinter trägt auch ohne Generator. Eine Error-Regel sagt: „Ein Verstoß darf das Ziel unter keinen Umständen erreichen.“ Genau das ist die Aufgabenbeschreibung eines Constraints. Eine Warnung sagt: „Ein Verstoß soll auffallen, aber nichts anhalten.“ Ein Constraint kann diese Aufgabe gar nicht erfüllen, denn er kennt nur zwei Ausgänge: Die Zeile passt, oder das Statement bricht ab. Eine Regel, die beobachten soll, hat im Ziel-Schema deshalb nichts verloren.
Genauso wichtig ist, was der Schweregrad nicht auslöst: Eine Regel mit Schweregrad E erzeugt nicht automatisch ein Constraint. Sie kann eines bekommen, und ob es angelegt wird, entscheidet das Datenmodell. Sauber trennen lassen sich dabei die Rollen: Die Regel stellt die Datenqualität im ETL-Prozess sicher. Sie findet Verstöße, protokolliert sie und hält sie am Quality Gate auf, bevor geladen wird. Das Constraint sichert die Datenqualität in der Datenbank ab. Es gilt auf jedem normalen Schreibweg der Datenbank, auch außerhalb des ETL-Prozesses. Die Zuordnung selbst ist dabei eine Entscheidung dieses Modells, keine Engine-Regel, denn Postgres kennt keine Schweregrade. Dass nur Error-Regeln ein Schema-Gegenstück bekommen, ist die Logik des Frameworks: Eine Regel, deren Verstoß hart abgewiesen werden soll, ist hier per Definition ein Error.
Für die Alters-Regel ist diese Entscheidung im Demo-Modell gefallen. Ihr Gegenstück steht als CHECK im Ziel-Schema:
1: CREATE TABLE core.customer
2: (
3: customer_id int NOT NULL
4: ,email text
5: ,phone text
6: ,age int
7: ,CONSTRAINT pk_customer PRIMARY KEY (customer_id)
8: ,CONSTRAINT ck_customer_age CHECK (age >= 0 AND age <= 120)
9: );
Die E-Mail-Regel taucht hier absichtlich nicht auf. Die Zeilen ohne E-Mail stehen nach dem Load im Ziel, ihre Warnung ist protokolliert, und der Prozess lief durch. Wer denselben Verstoß dagegen am Constraint vorbeischieben will, erlebt den zweiten Ausgang:
1: INSERT INTO core.customer (customer_id, email, phone, age)
2: VALUES (2, 'b@example.com', NULL, 200);
3: -- ERROR: new row for relation "customer" violates check constraint "ck_customer_age"
Derselbe fachliche Verstoß, zwei völlig verschiedene Konsequenzen. In der Pipeline war Alter 200 ein Befund in der Fehlertabelle. Am Ziel ist er ein abgebrochenes Statement. Ob eine Regel nur den ersten Weg kennt oder beide, steht in ihrem Schweregrad — deshalb ist er ein Routing-Entscheid und kein Etikett. Das ganze Routing auf einen Blick:
Vier Orte, eine Entscheidung
Zieht man die Perspektive auf, gibt es vier Orte, an denen eine Datenqualitäts-Regel leben kann. Jeder kann etwas, das die anderen nicht können:
| Ort | Was er kann | Was er kostet |
|---|---|---|
| Quelle (Vorsieb) | alle Verstöße vollständig melden, bevor der Load sie trifft, und den Prozess weiterlaufen lassen | eigene Prüf-Infrastruktur, wirkt nur im ETL-Pfad |
| Pipeline-Tool (dbt tests, Great Expectations, Soda) | Regeln als versionierter Code, CI-fähig, engine-übergreifend | die Regel lebt neben den Daten, jeder Weg an der Pipeline vorbei umgeht sie |
| Ziel-Schema (Constraint) | garantiert immer, auch bei Direkt-Zugriffen und Neben-Prozessen | kennt nur zwei Ausgänge, bricht Statements hart ab |
| Reporting | Trends, Überblick, Kommunikation an die Fachseite | setzt nichts durch |
In modernen Daten-Stacks liegt der Default heute meist in der Pipeline. dbt tests, Great Expectations und Soda haben dafür gute Gründe: Die Regeln liegen als Code im Repository, laufen in der CI, und dieselbe Prüfung funktioniert gegen Postgres wie gegen ein Cloud-Warehouse. Das ist ein legitimer Trade-off und kein Fehler.
Seinen Preis sollte man trotzdem kennen. Eine Regel im Pipeline-Tool gilt genau dann, wenn die Pipeline läuft. Der Kollege mit dem Ad-hoc-INSERT, das Migrationsskript vom letzten Quartal und der zweite Prozess, der dieselbe Tabelle beschreibt, sehen die Regel nicht. Ein Constraint im Ziel-Schema kennt diese Lücke nicht, denn er ist Teil der Daten selbst. Dafür bezahlt man mit seiner Kompromisslosigkeit. Die Abwägung folgt zwei Fragen: Der Schweregrad bestimmt, wie auf einen Verstoß reagiert wird. Ob eine Regel zusätzlich im Schema garantiert wird, entscheiden das Datenmodell und das, was ein Constraint ausdrücken kann. Was beobachten soll, gehört in Pipeline oder Vorsieb.
Warum das Vorsieb trotzdem vorab prüft
Wenn eine Error-Regel am Ziel als Constraint steht, wirkt die Vorab-Prüfung in der Quelle auf den ersten Blick doppelt. Sie ist es nicht, denn die beiden Ebenen beantworten verschiedene Fragen.
Der Constraint garantiert. Er ist die letzte Verteidigungslinie, die niemand umgehen kann. Aber er kennt nur seine zwei Ausgänge, und beim Laden von tausenden Zeilen ist „Statement bricht ab“ die schlechteste aller Antworten: Der Prozess steht, und welche Zeilen das Problem waren, weiß man auch nicht. Das Vorsieb beantwortet die andere Frage. Es identifiziert alle Sätze, die am Ziel scheitern würden, protokolliert sie mit Schlüssel, Wert und Meldung und lässt die sauberen weiterlaufen. Wie dieses Vorsieb aufgebaut ist — Fehlertabelle, Regel-Config, generischer Runner —, beschreibt der Sub-Hub Datenqualität mit SQL prüfen.
Es gibt noch eine dritte Variante, und viele Lade-Strecken sind genau so gebaut: gar keine explizite Prüfung, sondern die Bedingungen direkt im Lade-Statement. Das INSERT … SELECT bekommt eine WHERE-Klausel, die alle bekannten Fehlerbilder ausschließt, vom Alters-Bereich über die fehlende E-Mail bis zum unbekannten Länderkürzel, und geladen wird, was übrig bleibt. In einem dbt-Modell oder einem Talend-Job sieht das nur syntaktisch anders aus. Für eine Handvoll Bedingungen ist das der kürzeste Weg, und genau deshalb beginnen viele Strecken so.
Was dabei verloren geht, sieht man erst im Betrieb. Die Klausel wächst mit jeder neuen Regel zu einem Prädikat, das niemand mehr vollständig liest, und die Prüflogik ist im Lade-Code vergraben, statt als Konfiguration lesbar zu sein. Vor allem aber verschwinden die aussortierten Zeilen stumm: kein Befund, keine Meldung, kein Zähler. Dass heute 500 Sätze weniger ankamen und warum, erfährt niemand. Und Schweregrade kennt so ein Inline-Filter nicht, er kennt nur drin oder draußen — das ist das binäre pass/fail vom Anfang, nur versteckt. Das Vorsieb ist dieselbe Logik, einmal umgedreht: Jede Bedingung wird zur Regel-Zeile mit Schweregrad und Meldung, jeder Treffer zum protokollierten Befund, und das Lade-Statement behält eine einzige, stabile Bedingung: sys_error = 0.
Der Gewinn zeigt sich dann in allem, was hinter dem Gate kommt. Sobald die schlechten Sätze markiert und aussortiert sind, arbeitet jede nachgelagerte Strecke auf einem geprüften Bestand. Transformationen, Historisierung, Kennzahlen-Berechnung: Alles dahinter sind saubere, einfache SQL-Statements ohne eine einzige Abwehr-Bedingung, und dasselbe gilt für das dbt-Modell oder jedes andere Artefakt, das an dieser Stelle steht. Niemand muss beim Schreiben mehr daran denken, dass ein Alter 200 sein könnte oder ein Länderkürzel ins Leere zeigt. Diese Denk-Last ist an einer Stelle konzentriert, dem Vorsieb, statt über jeden nachgelagerten Schritt verteilt.
Beides zusammen ist kein Widerspruch, sondern Arbeitsteilung: Die Regel stellt die Datenqualität über den ETL-Prozess sicher, damit der Load nicht bricht. Das Constraint sichert die Datenqualität in der Datenbank ab, damit auch der Weg an der Pipeline vorbei keine schlechten Daten hinterlässt. Gleichrangig sind die beiden Ebenen dabei nicht: Der harte Constraint ist optional und eine Entscheidung des Datenmodells. Steht er aber im Schema, ist die Vorab-Prüfung Pflicht, denn ohne sie bricht der Load an genau diesem Constraint ab. Die Frage ist also nie, ob Schema oder Pipeline. Die Frage ist, welche Regel zusätzlich eine Schema-Garantie bekommt, und die Antwort steht im Schweregrad.
Was beim Hochstufen passiert
Eine Regel hochzustufen sieht harmlos aus. In der Config ist es ein UPDATE:
1: UPDATE
2: dq.check_rule
3: SET
4: severity = 'E'
5: WHERE
6: table_name = 'customer'
7: AND check_column = 'email';
Ab dem nächsten Lauf blockiert die E-Mail-Regel das Gate. Das ist der kleine Teil der Änderung. Der große Teil folgt aus dem Routing: Soll die Regel auch hart garantiert sein, gehört jetzt ein CHECK ins Ziel-Schema. Das Hochstufen ist dann der eine Fall, in dem die Regel vor der Schema-Garantie da ist und das Datenmodell nachziehen muss. Und im Ziel wartet der Bestand, inklusive der Zeilen, die unter der alten Warnung völlig legal ohne E-Mail eingelaufen sind.
Postgres bietet für genau diese Situation einen zweistufigen Weg. NOT VALID nimmt den Constraint sofort für neue Zeilen scharf und lässt den Bestand zunächst ungeprüft:
1: ALTER TABLE core.customer
2: ADD CONSTRAINT ck_customer_email CHECK (email IS NOT NULL) NOT VALID;
Neue Zeilen ohne E-Mail werden ab diesem Moment abgewiesen. Die Validierung des Bestands ist der zweite, getrennte Schritt, und der scheitert ehrlich, solange Altlasten existieren:
1: ALTER TABLE core.customer VALIDATE CONSTRAINT ck_customer_email;
2: -- ERROR: check constraint "ck_customer_email" of relation "customer" is violated by some row
Erst wenn der Bestand bereinigt ist — nachgepflegt, archiviert oder bewusst gelöscht, das ist eine fachliche Entscheidung —, läuft VALIDATE CONSTRAINT durch, und die Regel gilt hart. Das ist der eigentliche Preis des Hochstufens: Aus einer Beobachtung wird eine Garantie, und eine Garantie muss auch für alles gelten, was schon da ist. Wie man solche nachträglichen Verschärfungen an einer befüllten Tabelle ohne Downtime durchzieht, zeigt im Detail der Artikel NOT-NULL-Spalte nachträglich hinzufügen.
Für SQL-Server-Leser: Das dortige WITH NOCHECK sieht ähnlich aus, hat aber eine andere Semantik. Der Constraint bleibt „not trusted“, bis er mit WITH CHECK CHECK CONSTRAINT nachvalidiert wird, und der Optimizer ignoriert ihn bis dahin bei seinen Annahmen. Der Postgres-Weg und der SQL-Server-Weg lösen dasselbe Problem, sind aber nicht austauschbar.
Die Grenzen, ehrlich
Die Entsprechung zwischen Error-Regel und Constraint hat eine Grenze, und die liegt nicht im Schweregrad, sondern in dem, was ein Constraint ausdrücken kann.
Ein CHECK sieht genau eine Zeile. Feldübergreifende Regeln innerhalb der Zeile kann er noch tragen, etwa „Enddatum liegt nach dem Startdatum“. Sobald die Regel über Zeilen hinweg schaut, ist Schluss: Ein „höchstens drei Verträge je Kunde“ ist mit Bordmitteln kein Constraint mehr, ein UNIQUE deckt nur den Spezialfall ab, dass eine Wert-Kombination höchstens einmal vorkommen darf. Tabellenübergreifende Fachregeln wie „Rabatt nur bei aktivem Rahmenvertrag“ scheitern ebenso, denn der Fremdschlüssel prüft Existenz, nicht Fachlogik.
Auch zeitliche Regeln gehören nicht in ein CHECK, selbst wenn Postgres sie syntaktisch zulässt. Ein CHECK (order_date <= current_date) ist eine Falle: Eine Zeile, die heute gültig ist, wäre bei einem Restore morgen immer noch gültig, aber eine Prüfung gegen „jetzt“ ist nicht stabil reproduzierbar, und Dump-Reihenfolgen oder nachträgliche Validierungen können an ihr scheitern. Constraints sollten auf unveränderlichen Ausdrücken stehen, deren Ergebnis für dieselbe Zeile stets gleich bleibt.
Für die zeilen- und tabellenübergreifenden Fälle gibt es allerdings einen Ausweg, und er verdient einen ehrlichen Blick: den Trigger. Ein BEFORE-Trigger darf abfragen, was ein CHECK nicht sehen kann, also andere Zeilen und andere Tabellen, und bei einem Verstoß eine Exception werfen. Ein „höchstens drei Verträge je Kunde“ lässt sich so tatsächlich in der Datenbank erzwingen, mit derselben Reichweite wie ein Constraint: auf jedem Schreibweg, auch an der Pipeline vorbei.
Der Preis ist erheblich, und er hat drei Posten. Erstens die Laufzeit: Der Trigger feuert bei jedem INSERT und UPDATE und führt dabei eigene Abfragen aus, was einen Massen-Load spürbar bremst. Zweitens die Parallelität: Zwei gleichzeitige Transaktionen sehen die uncommitteten Zeilen der jeweils anderen nicht, und beide können die „höchstens drei“-Prüfung gleichzeitig bestehen. Wasserdicht wird die Regel erst mit einer zusätzlichen Absicherung gegen parallele Transaktionen, etwa explizitem Sperren oder der Isolationsstufe SERIALIZABLE, also mit genau dem Aufwand, den einem ein echtes UNIQUE abnimmt. Drittens die Sichtbarkeit: Ein Constraint steht deklarativ im Schema und ist für jeden lesbar, ein Trigger versteckt dieselbe Regel in prozeduralem Code, und ein zweistufiges NOT VALID/VALIDATE für den Bestand gibt es für ihn nicht. Ein Trigger bleibt damit die begründete Ausnahme für den Fall, dass eine zeilen- oder tabellenübergreifende Garantie wirklich hart sein muss. Als Default taugt er nicht.
Für alle übrigen Regeln ist die Pipeline nicht die Notlösung, sondern der richtige Ort. Sie kann über Zeilen, Tabellen und Zeit hinweg prüfen, und sie kann das Ergebnis differenziert melden statt hart abzubrechen. Eine Error-Regel dieser Art bleibt also Pipeline-Regel — sie blockiert das Gate, ohne je ein Gegenstück im Schema zu bekommen. Die Entsprechung gilt darum mit einer Bedingung: Nur was ein Constraint ausdrücken kann, steht im Ziel-Schema auch als Constraint.
Entscheidungsmatrix
Die Zusammenfassung als Tabelle. Sie beantwortet für die häufigsten Regel-Arten beide Fragen auf einmal: welcher Schweregrad, und wo die Regel durchgesetzt wird.
| Regel-Art | Schweregrad | Durchsetzungs-Ort |
|---|---|---|
| Pflichtfeld, das das Ziel-Schema erzwingt | E | Vorsieb in der Quelle plus NOT NULL/CHECK am Ziel |
| Wertebereich mit fachlicher Härte (Alter 0..120) | E | Vorsieb plus CHECK am Ziel |
| Eindeutigkeit des fachlichen Schlüssels | E | Vorsieb plus UNIQUE am Ziel |
| Referenz auf Stammdaten | E | Vorsieb plus FOREIGN KEY am Ziel |
| Feldvergleich innerhalb einer Zeile (Enddatum nach Startdatum) | E | Vorsieb plus CHECK am Ziel |
| Plausibilität (Alter unter 18) | W | Pipeline-Finding, kein Constraint |
| Vollständigkeits-Beobachtung (Telefon fehlt) | I | Pipeline-Finding |
| Zeilen- oder tabellenübergreifende Bedingung | E oder W | Pipeline (ein Constraint kann es nicht, ein Trigger nur als begründete Ausnahme) |
| Zeitliche Regel (Datum nicht in der Zukunft) | W oder E | Pipeline (ein CHECK gegen „jetzt“ ist instabil — auch als Error bleibt sie Pipeline-Regel) |
Zwei Lesarten stecken in der Tabelle. Erstens: Der Schweregrad allein entscheidet die Orts-Frage nicht. Ein E macht eine Regel zum Kandidaten für ein Constraint-Gegenstück, aber nur dann, wenn sich ihre Bedingung überhaupt als Constraint schreiben lässt. Die zeilen- oder tabellenübergreifende Error-Regel bleibt deshalb trotz E in der Pipeline. Zweitens: Bei den Regeln, die ein Schema-Gegenstück haben, heißt E „doppelt“ — vorab geprüft in der Quelle und garantiert am Ziel. Das ist kein Widerspruch, sondern der Kern der Arbeitsteilung.
FAQ
Weil ein einzelnes Bit den Unterschied zwischen „Datensatz unbrauchbar“ und „Schönheitsfehler“ nicht transportieren kann. In der Praxis endet binäres pass/fail an einem der beiden Enden: Entweder blockieren Bagatellen den Load, bis jemand die Prüfung abschaltet, oder alles läuft durch und niemand liest die Befunde. Drei Schweregrade trennen „blockieren“ von „beobachten“ und halten das System dadurch am Leben.
Nur Error-Regeln, deren Bedingung eine einzelne Zeile mit unveränderlichen Ausdrücken prüfen kann, also Wertebereiche, Pflichtfelder, Format-Grenzen und Feldvergleiche innerhalb derselben Zeile. Eindeutigkeit übernimmt ein UNIQUE-Constraint, Referenzen ein FOREIGN KEY. Warnungen und Informationen bekommen nie ein Constraint-Gegenstück, denn ein Constraint kann nur blockieren und nicht beobachten. Und ein CHECK ersetzt kein NOT NULL: Ergibt seine Bedingung wegen eines NULL-Werts UNKNOWN, gilt sie als nicht verletzt. Die Pflicht zur Befüllung modelliert erst NOT NULL.
Nein. Eine Warnung wird mit Schlüssel, Wert und Meldung in der Fehlertabelle protokolliert und zählt im Warnungs-Zähler des Datensatzes, aber das Quality Gate filtert ausschließlich auf den Error-Zähler. Der Datensatz fließt weiter. Genau diese Eigenschaft unterscheidet die Warnung vom Error und macht sie zum richtigen Schweregrad für die meisten realen Datenprobleme.
In der Regel-Config ist es ein UPDATE, ab dem nächsten Lauf blockiert die Regel das Gate. Soll sie zusätzlich als Constraint ins Ziel-Schema, muss der Bestand sauber sein: In Postgres legt man den Constraint mit NOT VALID an, der sofort für neue Zeilen gilt, und validiert den Bestand nach der Bereinigung mit VALIDATE CONSTRAINT. Das Hochstufen ist damit weniger eine Konfigurations-Änderung als ein kleines Daten-Projekt.
Beides hat seinen Platz, und die Frage ist keine Entweder-oder-Entscheidung. Pipeline-Tools versionieren Regeln als Code, laufen in der CI und arbeiten engine-übergreifend. Was aber unter keinen Umständen im Ziel landen darf, sollte zusätzlich als Constraint in der Datenbank stehen, denn nur dort gilt die Regel auch für Prozesse, die an der Pipeline vorbeischreiben. Der Schweregrad einer Regel ist ein brauchbares Kriterium für genau diese Aufteilung.
Verwandte Artikel
Framework und Routinen:
- Datenqualität mit SQL prüfen — der Sub-Hub: das konfigurierbare Framework mit Fehlertabelle, Regel-Config, Runner und dem Quality Gate, dessen Schweregrad-Logik dieser Artikel vertieft.
- Daten mit SQL validieren — Wertebereiche, Pflichtfelder und die NULL-Falle: die Routine hinter den WHERE-Regeln.
- Duplikate finden mit SQL — die Eindeutigkeits-Routine, deren Error-Regeln am Ziel ihr Gegenstück im
UNIQUE-Constraint haben. - Verwaiste Datensätze finden mit SQL — die Referenz-Routine, das Vorsieb vor dem
FOREIGN KEY. - Prüfregeln aus dem Schema ableiten — was die Metadaten schon wissen: wie die Error-Regeln aus dem restriktiven Datenmodell abgeleitet werden, statt sie von Hand zu tippen. (erscheint in Kürze)
Theorie und Architektur:
- Datenqualität: Dimensionen und Fehlerklassen — der Konzept-Rahmen der Serie: welche Dimensionen es gibt und welche SQL erreicht.
- Design Pattern // Architektur eines ETL-Prozesses — wo schlechte Daten in geschichteten Staging-Ebenen isoliert werden.
Ziel-Schema:
- Postgres-Tabellen-Konventionen — welche Keys und Constraints eine Zieltabelle tragen sollte.
- NOT-NULL-Spalte nachträglich hinzufügen — das Expand/Contract-Muster für nachträgliche Verschärfungen an befüllten Tabellen.