Ein Feld, ein Default, ein nächtlicher Batch. Der Batch lief ein Jahr lang gut, dann kam eine Spalte dazu, und die Buchhaltung bekam null Zeilen statt vierzigtausend. Niemand hatte etwas kaputt gemacht. Genau das war das Problem.


Der Abend, an dem das Schema mitredete

Viertel nach zwei, ein Anruf, ein Job rot. Die Meldung im Log war die Höflichkeitsform: Datenqualität. Gemeint war, dass der Join auf einer Spalte lief, die seit drei Wochen existierte und in allen alten Zeilen denselben Platzhalter enthielt. Der Job hat nicht versagt. Er hat sauber nichts gefunden und das als Erfolg gemeldet, mit grünem Haken und Laufzeit 41 Minuten. Nachts um zwei ist ein grüner Haken das Schlimmste, was dir passieren kann, weil dann niemand mehr nachsieht.


Ein Schema ist ein Vertrag, den niemand unterschrieben hat

Vier Abteilungen lesen aus derselben Tabelle. Eine schreibt hinein. Keine besitzt sie. Das ist der Normalzustand in gewachsenen Systemen, und er funktioniert erstaunlich lange, weil alle vorsichtig sind. Bis jemand eine Spalte braucht, für die es keinen Termin gibt, und dann ALTER TABLE tippt.


Das Datenmodell dazu ist sechs Jahre alt und liegt als PDF im Wiki. Vier der acht Tabellen darauf existieren nicht mehr, zwei fehlen. Niemand hat es aktualisiert, weil niemand dafür zuständig ist, und das ist derselbe Grund, aus dem die Tabelle keinen Owner hat. Ein Dokument ohne Owner veraltet so zuverlässig wie ein Feld ohne Leser.


ALTER TABLE ist eine einseitige Vertragsänderung. Kein Leser wird gefragt, kein Leser erfährt davon, und die Änderung ist in dreißig Sekunden durch. Genau deshalb passiert sie nachts, kurz vor dem Deployment, wenn niemand mehr widerspricht.


flowchart LR E["Einkauf schreibt"] --> S["SCHEMA order: bestellung, bestellung_pos, kunde"] BU["Buchhaltung liest"] --> S L["Logistik liest"] --> S S --> R["Reporting liest"] O["Owner: keine Abteilung, seit sechs Jahren"] -.-> S

Die Migration, die drei Wochen später weh tat

So sah die Änderung aus, die den Abend verursacht hat. Sie ist technisch korrekt. Sie hat einen Default, sie hat keine Sperre über Minuten, sie läuft in einem Fenster, in dem niemand arbeitet. Sie ist nur fachlich falsch.


Symbolbild: KI-generiert für diesen Artikel (Flux).
-- Einkauf, Migration "schnell noch ein Feld"
ALTER TABLE bestellung
    ADD COLUMN lieferanten_nr VARCHAR(20) NOT NULL DEFAULT 'UNBEKANNT';

-- drei Wochen spaeter, Job der Buchhaltung
SELECT k.nr, b.lieferanten_nr
FROM   konto k
JOIN   bestellung b ON b.konto_nr = k.nr
WHERE  b.lieferanten_nr != 'UNBEKANNT';   -- 0 Zeilen, 41 Minuten

Im Ticket stand: Feld hinzufügen, ein Aufwandspunkt. Es war ein Aufwandspunkt. Die Migration lief in elf Sekunden, der Review bestand aus zwei Kommentaren zur Benennung, und der Freigabeprozess bestand aus einem Haken in einem Formular. Für die Frage, wer die Spalte später liest, gab es kein Feld im Formular.


Der Platzhalter war die Absicht. Er sollte signalisieren, dass hier noch nichts steht. Er hat stattdessen signalisiert, dass hier nichts steht, und zwar in jeder Zeile, die vor der Migration entstanden ist. Wer den Filter schreibt, denkt an fehlende Daten und nicht an die Vergangenheit.


Die Reparatur hat zwei Tage gedauert: Backfill aus der Lieferantenstammdatentabelle, ein zusätzlicher Index, ein Rückbau des Filters. Die Erklärung im Betriebshandbuch hat drei Wochen gedauert, weil sie durch vier Abteilungen musste, von denen zwei die Tabelle nicht kannten und eine sie besaß, ohne es zu wissen.


Wie ein Vertrag aussieht, den man prüfen kann

Ein Data Contract ist kein Dokument, das jemand unterschreibt und einrahmt. Es sind vier bis sechs Regeln, die im Migrationswerkzeug stehen und den Build rot machen. Alles andere ist Absichtserklärung.


KlauselWas sie verhindertKosten
Spalten nur additiv, nie umbenennenstille Brüche bei Lesernnull
Neue Pflichtspalte nur mit BackfillPlatzhalter in Altdatenein Skript
Bedeutung einer Spalte ist unveränderlichUmdeutung ohne Migrationeine neue Spalte
Löschen erst nach zwei Quartalen ohne LeserLöschen des falschen Feldesein Zähljob

Die vierte Zeile ist die einzige, die mich in vier Jahren einmal vor einem Datenverlust bewahrt hat. Sie kostet einen Zähljob auf den Zugriffsstatistiken, den man einmal eingerichtet hat und danach nie wieder anschaut, bis er etwas findet.


Was ich in den Vertrag schreibe

Ich schreibe inzwischen fünf Zeilen in jede Schema-Beschreibung, und ich bestehe darauf, dass sie im Repository steht und nicht im Wiki. Das Wiki ist der Ort, an dem Zusagen hingehen, wenn sie niemand mehr prüft.


  • Owner: eine Abteilung, ein Name, eine Vertretung. Nicht ein Gremium.

  • Leser: wer liest welche Spalte und mit welcher Frequenz. Einmal im Quartal aus den Zugriffsstatistiken erneuert.

  • Garantien: welche Spalten nie NULL sind, welche Wertebereiche gelten, was ein Platzhalter bedeutet.

  • Änderungsweg: wer eine Spalte hinzufügen darf und wer sie freigibt. Zwei Namen, nicht ein Prozess.

  • Depot: wo die Migration liegt und wie man sie zurückrollt. Ein Rollback, den niemand geübt hat, ist eine Hoffnung.

Das klingt nach Papierkram. Es ist Papierkram, der einen Anruf um Viertel nach zwei verhindert, und damit die billigste Form von Betrieb, die ich kenne.


Symbolbild: KI-generiert für diesen Artikel (Flux).

Fazit

Ein Schema ohne Owner ist ein Vertrag ohne Partei: jeder darf ihn ändern, niemand haftet. Der Preis zeigt sich nicht beim Deployment, sondern drei Wochen später, wenn ein Job null Zeilen findet und Erfolg meldet. Das kostet zwei Tage Reparatur, drei Wochen Dokumentation und eine Nacht, in der du darüber nachdenkst, warum grün nicht grün bedeutet.


Schreib den Owner hin. Zähl die Leser. Und wenn du eine Spalte mit Platzhalter anlegst, frag vorher, wer sie in einem Jahr als Wahrheit liest.


Nenn mir die Spalte in deinem Schema, die drei Teams liest und keiner besitzt — wie lange steht sie schon da?