Drei Schweregrade statt pass/fail — und warum der Schweregrad entscheidet, wo die Regel steht

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– oder UNIQUE-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 VALID und VALIDATE CONSTRAINT zeigen das im Zeitraffer.
  • Die ehrliche Grenze: Ein CHECK darf 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

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_idemailphoneageBefund
1a@example.com+49 30 11111130sauber
2b@example.comNULL200Alter unmöglich (E), Telefon fehlt (I)
3NULL+43 1 22222245E-Mail fehlt (W)
4NULLNULL25E-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_idsys_errorsys_warningsys_info
1000
3010
4011

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:

Datenfluss von Staging über das Quality Gate ins Ziel-Schema: Nur die E-Regel blockiert am Gate, W- und I-Befunde werden nur protokolliert, und ein gestrichelter Pfeil zeigt das optionale CHECK/UNIQUE-Gegenstück der E-Regel im Ziel-Schema.

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:

OrtWas er kannWas er kostet
Quelle (Vorsieb)alle Verstöße vollständig melden, bevor der Load sie trifft, und den Prozess weiterlaufen lasseneigene Prüf-Infrastruktur, wirkt nur im ETL-Pfad
Pipeline-Tool (dbt tests, Great Expectations, Soda)Regeln als versionierter Code, CI-fähig, engine-übergreifenddie Regel lebt neben den Daten, jeder Weg an der Pipeline vorbei umgeht sie
Ziel-Schema (Constraint)garantiert immer, auch bei Direkt-Zugriffen und Neben-Prozessenkennt nur zwei Ausgänge, bricht Statements hart ab
ReportingTrends, Überblick, Kommunikation an die Fachseitesetzt 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-ArtSchweregradDurchsetzungs-Ort
Pflichtfeld, das das Ziel-Schema erzwingtEVorsieb in der Quelle plus NOT NULL/CHECK am Ziel
Wertebereich mit fachlicher Härte (Alter 0..120)EVorsieb plus CHECK am Ziel
Eindeutigkeit des fachlichen SchlüsselsEVorsieb plus UNIQUE am Ziel
Referenz auf StammdatenEVorsieb plus FOREIGN KEY am Ziel
Feldvergleich innerhalb einer Zeile (Enddatum nach Startdatum)EVorsieb plus CHECK am Ziel
Plausibilität (Alter unter 18)WPipeline-Finding, kein Constraint
Vollständigkeits-Beobachtung (Telefon fehlt)IPipeline-Finding
Zeilen- oder tabellenübergreifende BedingungE oder WPipeline (ein Constraint kann es nicht, ein Trigger nur als begründete Ausnahme)
Zeitliche Regel (Datum nicht in der Zukunft)W oder EPipeline (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

Warum reicht pass/fail für Datenqualität nicht?

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.

Welche Prüfregeln gehören in ein CHECK-Constraint?

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.

Blockiert eine Warnung den Load?

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.

Was passiert, wenn ich eine Warnung zu einem Error hochstufe?

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.

Gehört Datenqualität in dbt oder Great Expectations statt in die Datenbank?

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:

Ziel-Schema: