Der erste Blick in das Schema war ein DESCRIBE auf die Tabelle, um die sich alles dreht:

kd_auftrag_pos_hist: 62 Spalten
  id            bigint
  kdnr          varchar(12)
  pos_nr        smallint
  art_nr        varchar(20)
  art_nr_alt    varchar(20)
  menge         decimal(12,3)
  preis         decimal(12,4)
  preis2        decimal(12,4)
  ...

Achtzehn Jahre Wachstum, kein Kommentar, ein Trigger mit dem Namen tr_pos_hist_upd aus dem Jahr 2011 und eine Spalte preis2, zu der mir drei Kollegen drei verschiedene Erklärungen gegeben haben. Ich hatte zwei Wochen und die ausdrückliche Anweisung, nichts zu ändern.

Tag eins: nichts anfassen

Die erste Entscheidung war, einen read-only User anzulegen und alles andere über ihn zu machen. Das klingt nach Vorsicht und war Selbstschutz: In einer Datenbank, in der _alt und _hist als Suffixe existieren, ist ein versehentliches UPDATE ein Datenverlust mit Zeitstempel.

Dann habe ich gezählt. Nicht die Tabellen, die Zeilen – weil die Zeilenzahl sagt, welche Tabelle lebt und welche nur noch existiert.

import psycopg2

SQL = (
    "SELECT relname, n_live_tup "
    "FROM pg_stat_user_tables "
    "ORDER BY n_live_tup DESC"
)

with psycopg2.connect(dsn) as conn, conn.cursor() as cur:
    cur.execute(SQL)
    for name, rows in cur.fetchall()[:15]:
        print(f"{rows:>12,}  {name}")

Von 214 Tabellen hatten 61 exakt null Zeilen und 22 weniger als zehn. Das sind keine Tabellen, das sind Grabsteine. Ich habe sie nicht gelöscht, ich habe sie markiert – und in der Dokumentation steht jetzt bei jeder: „leer seit mindestens 2019, nicht löschen, weil ein Report sie referenziert.“

Was die Spaltennamen verraten

_hist heißt Historie, aber nicht bei jeder Tabelle dasselbe: Bei kd_auftrag ist es ein Append-Only-Log, bei kd_auftrag_pos eine Kopie des aktuellen Stands mit Trigger. _alt heißt „vor der Migration 2016“, wird aber in zwei Prozeduren noch geschrieben. _tmp heißt bei drei Tabellen „temporär“ und bei einer „bitte nicht benutzen“, was niemand aufgeschrieben hat.

Die zweite Quelle waren die Fremdschlüssel – oder ihr Fehlen. 38 Spalten heißen kdnr, aber nur elf haben einen Foreign Key. Die anderen 27 sind Join-Felder, die irgendwann funktioniert haben und heute auf Werte zeigen, die es nicht mehr gibt. Ich habe die verwaisten gezählt: 4.112 Zeilen in kd_auftrag_pos_hist mit einer kdnr, die in kd_kunde nicht existiert.

Ausdruck eines Datenbankschemas mit handschriftlichen Notizen und Verbindungslinien
Mein Ausdruck nach Tag drei: 214 Tabellen, 61 davon leer. Die gelben Markierungen sind Tabellen, die ein Report referenziert – die dürfen nicht weg, egal wie tot sie aussehen.

Die Karte, die keiner hatte

Die vierte Quelle war der Anwendungscode, und sie war die langsamste. Ich habe die SQL-Strings aus dem Repo extrahiert und nach Tabellennamen gruppiert, um zu sehen, welche Tabellen überhaupt gelesen werden. Ergebnis: 118 der 214 kommen in keiner Query vor, werden aber von zwei Reports referenziert, die einmal im Monat laufen. Das ist die Sorte Abhängigkeit, die man erst bemerkt, wenn man sie entfernt.

Nach einer Woche hatte ich sechs Tabellen, die den Geschäftsfall tragen, und ein Diagramm, das auf eine Seite passt. Das ist der einzige Teil der Dokumentation, den ich für unverzichtbar halte, weil er die Frage beantwortet, die jeden Neueinsteiger blockiert: Wo fange ich an?

erDiagram KUNDE ||--o{ AUFTRAG : hat AUFTRAG ||--|{ POSITION : enthaelt POSITION }o--|| ARTIKEL : verweist AUFTRAG ||--o{ RECHNUNG : erzeugt RECHNUNG ||--o{ ZAHLUNG : beglichen_durch ARTIKEL ||--o{ LAGER : liegt_in

Sechs Entitäten, fünf Beziehungen, und im Absatz darunter steht, was fehlt: die Historientabellen, die Trigger, die 27 Join-Felder ohne Constraint. Ein Diagramm, das vollständig sein will, wird unlesbar. Ein Diagramm, das ehrlich sagt, was es weglässt, wird benutzt.

Dokumentation, die nicht veraltet

Mein erster Impuls war ein Wiki. Mein zweiter war die Erkenntnis, dass ein Wiki nach sechs Monaten genauso falsch ist wie keine Dokumentation, nur überzeugender. Also habe ich drei Dinge gebaut, die sich selbst prüfen.

Kommentare in der Datenbank: COMMENT ON COLUMN für die 40 Spalten, bei denen ich sicher war. Sie stehen dort, wo man sie liest – im DESCRIBE – und sie wandern mit jedem Dump mit.

Ein Test in der CI, der prüft, ob die dokumentierten Spalten noch existieren. Wenn jemand eine Spalte umbenennt, wird der Build rot, und das ist der einzige Mechanismus, der gegen stille Drift hilft.

Ein Wörterbuch, das aus information_schema generiert wird, nicht gepflegt. Es beschreibt Typen, Nullability und Fremdschlüssel – alles, was die Datenbank über sich selbst weiß, und nichts, was ein Mensch behaupten müsste.

Fazit

Achtzehn Jahre ohne Dokumentation sind kein Versäumnis, sie sind eine normale Folge von Prioritäten: Das System lief, also gab es keinen Grund, es zu erklären. Der Preis wird nicht von denen bezahlt, die es gebaut haben, sondern von der nächsten Person, die eine Spalte preis2 findet und raten muss.

Meine Frage an dich: Wenn du morgen in ein Schema mit 200 Tabellen ohne Kommentar geworfen würdest – würdest du zuerst die Zeilenzahlen zählen oder zuerst den Code lesen, der sie schreibt?