In Gesprächen über Altsysteme geht es fast immer um Code. Welche Sprache, welches Framework, welche Version. Das ist die Ebene, auf der es wehtut, und es ist die Ebene, die sich am leichtesten ersetzen lässt.
Die Ebene, die bleibt, ist eine andere. Ein Team kann in zwei Jahren die komplette Anwendung neu bauen, die Sprache wechseln und das Framework tauschen. Das Schema der Datenbank überlebt das, weil daran alles hängt, was nicht im Repository steht: Berichte, Exporte, eine Schnittstelle zu einem Partner, ein Skript in der Buchhaltung, das seit 2017 jeden Monatsersten läuft.
Dieser Artikel handelt davon, wie man ein gewachsenes Schema einschätzt, welche Altlasten sich lohnen und welche man mitnimmt, und in welcher Reihenfolge man vorgeht, ohne den Betrieb anzuhalten.
Warum das Schema den Code überlebt
Code hat genau einen Nutzer: die Laufzeitumgebung. Ein Schema hat beliebig viele, und die meisten davon kennen Sie nicht.
Das ist keine Metapher. In jedem gewachsenen System, das ich gesehen habe, gab es mindestens einen Zugriff auf die Datenbank, der nicht durch die Anwendung lief. Ein Berichtswerkzeug mit eigenem Lesezugang. Ein zweites System, das nachts eine Tabelle ausliest. Eine Tabellenkalkulation, in der jemand vor Jahren eine Verbindung eingerichtet hat. Ein Skript auf dem Rechner eines Mitarbeiters.
Jeder dieser Zugriffe ist ein Vertrag, den niemand geschlossen hat und den niemand kündigen kann, weil niemand weiß, dass er existiert. Solange Sie nur Code ändern, merken diese Nutzer nichts. Sobald Sie eine Spalte umbenennen, merken sie es alle gleichzeitig, und Sie erfahren davon aus der Buchhaltung.
Daraus folgt die erste Regel, und sie ist unbequem: Das Schema wird anders behandelt als der Code. Beim Code ist Umbenennen billig, weil der Compiler oder die Suche alle Stellen findet. Beim Schema findet die Suche nur die Stellen, die im Repository stehen.
Die vier Altlasten, die fast immer da sind
Gewachsene Schemata unterscheiden sich weniger, als man denkt. Vier Muster finde ich in fast jedem, und sie kosten sehr unterschiedlich viel.
Erstens: keine Fremdschlüssel. Die Verknüpfung existiert, aber nur im Kopf und im Code. Die Folge merkt man erst bei der ersten Migration: Es gibt Bestellpositionen ohne Bestellung, Adressen ohne Kunden, Zahlungen auf gelöschte Rechnungen. Nicht viele, aber genug, dass jede Umstellung eine Sonderbehandlung braucht.
-- Wie viele Waisen gibt es wirklich? Vor jeder Migration einmal messen.
SELECT COUNT(*) AS bestellpositionen_ohne_bestellung
FROM bestellposition p
LEFT JOIN bestellung b ON b.id = p.bestellung_id
WHERE b.id IS NULL;Zweitens: Spalten, die zwei Dinge bedeuten. status mit den Werten 0 bis 9, von denen 7 und 8 dasselbe meinen, weil einmal jemand einen neuen Fall gebraucht hat. Oder ein Textfeld notiz, in dem seit 2019 auch die Rücksendenummer steht, erkennbar an einem Präfix. Solche Spalten sind der Grund, warum niemand die Auswertung nachbauen kann.
Drittens: alles ist Text. Beträge als VARCHAR, Daten als CHAR(10), Wahrheitswerte als 'J' und 'N', gelegentlich auch als 'ja'. Das ist die teuerste Altlast, weil sie sich nicht lokal beheben lässt: Jede Stelle, die den Wert liest, hat ihre eigene Umrechnung, und die stimmen nicht alle überein.
Viertens: nichts wird gelöscht. Eine Spalte geloescht, die von der Hälfte der Abfragen berücksichtigt wird und von der anderen nicht. Das ist die Altlast, die stille falsche Zahlen produziert: Zwei Berichte über dieselbe Sache liefern verschiedene Ergebnisse, und beide sind aus ihrer Sicht richtig.
Was Sie messen, bevor Sie etwas ändern
Bevor eine Entscheidung fällt, braucht es Zahlen. Drei Abfragen liefern die Grundlage, und sie dauern zusammen eine halbe Stunde.
Wie groß ist was, und wo liegt die Historie? Das entscheidet, ob eine Umstellung in ein Wartungsfenster passt oder in Portionen laufen muss.
SELECT table_name,
ROUND((data_length + index_length) / 1024 / 1024) AS mb,
table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY data_length + index_length DESC
LIMIT 20;Welche Tabellen werden überhaupt noch geschrieben? In einem zehn Jahre alten Schema sind regelmäßig zwanzig Prozent der Tabellen tot. Die muss niemand modernisieren, die muss jemand archivieren.
SELECT table_name, update_time, create_time
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
ORDER BY update_time IS NULL, update_time ASC;Die Zeitangabe ist je nach Speicher-Engine ungenau und reicht trotzdem: Eine Tabelle, die zuletzt 2021 angefasst wurde, ist ein Kandidat, und der Rest klärt sich im Gespräch.
Wer greift zu, und von wo? Das ist die wichtigste der drei Fragen und die einzige, die nicht im Schema steht. Die Benutzerliste der Datenbank ist der Anfang:
SELECT user, host FROM mysql.user ORDER BY user;Jeder Benutzer, der nicht die Anwendung ist, ist einer der ungeschriebenen Verträge von oben. Bei jedem gehört notiert, wem er gehört und was er liest. Wo das Protokoll langsamer Abfragen mitläuft, steht die Antwort dort schon.
Die Regel: erweitern, nie umbenennen
Wenn die Nutzer eines Schemas unbekannt sind, kann man es trotzdem ändern. Nur nicht in einem Schritt. Das Muster heißt Expand and Contract und besteht darin, dass Alt und Neu eine Weile nebeneinander existieren.
Für eine Spalte, deren Bedeutung sich ändert, sieht das so aus:
- Erweitern. Die neue Spalte entsteht, sauber getypt. Nichts liest sie.
- Doppelt schreiben. Der Code schreibt ab jetzt in beide Spalten. Gelesen wird weiter aus der alten. Ab hier ist alles, was neu hereinkommt, in beiden Formen vorhanden.
- Nachziehen. Der Bestand wird in Portionen übertragen, außerhalb der Auslieferung, mit einer Abbruchmöglichkeit. Danach: zählen, ob beide Spalten dasselbe sagen.
- Umschalten. Gelesen wird aus der neuen Spalte, geschrieben weiter in beide. Das ist der Punkt, an dem es auffällt, wenn ein unbekannter Nutzer die alte Spalte braucht, und zwar bevor sie verschwindet.
- Zusammenziehen. Erst nach einer Frist, in der nichts aufgefallen ist, entfällt das Schreiben in die alte Spalte, und erst danach wird sie entfernt.
Der Schritt, den alle kürzen wollen, ist die Frist zwischen 4 und 5. Sie muss länger sein als der längste Berichtszeitraum im Haus. Wenn jemand einmal im Quartal eine Auswertung fährt, sind zwei Wochen zu wenig, und Sie erfahren es im Quartal darauf.
Zwischen Schritt 3 und 4 gehört eine Prüfung, die sich lohnt, weil sie den einen Fall findet, der später teuer wird:
-- Wo sagen alte und neue Spalte etwas Verschiedenes?
SELECT id, status_alt, status_neu
FROM bestellung
WHERE status_neu IS DISTINCT FROM abgeleitet(status_alt)
LIMIT 50;Wie eine solche Erweiterung in Betrieb geht, ohne dass unterwegs Daten verloren gehen, steht in Dual-Write und Backfill.
Die Nutzer, die Sie nicht kennen
Weil diese Nutzer der eigentliche Grund für den ganzen Aufwand sind, lohnt es sich, sie aktiv zu suchen statt auf sie zu warten. Drei Wege funktionieren.
Das Protokoll der Verbindungen. Eine Woche lang aufzeichnen, welche Benutzer von welchen Adressen verbinden. Das ist die vollständigste Liste, die Sie bekommen können, und sie enthält regelmäßig Adressen, die niemand zuordnen kann.
Die Spalte, die niemand liest. Umgekehrt lässt sich vor dem Entfernen prüfen, ob eine Spalte überhaupt noch abgefragt wird. Wo das Abfrageprotokoll mitläuft, reicht eine Suche über eine Woche Protokoll; sonst hilft der Zwischenschritt, die Spalte für zwei Wochen auf NULL zu setzen statt sie zu entfernen. Beschwerden kommen dann mit Namen.
Die Sicht als Brücke. Wenn ein unbekannter Nutzer eine Tabelle liest, die verschwinden soll, kann an ihrer Stelle eine Sicht stehen, die dieselben Spalten aus der neuen Struktur liefert. Das kostet einmal Arbeit und nimmt den Zeitdruck aus der Umstellung, weil der fremde Zugriff weiterläuft.
Diese Sichten sind ausdrücklich als Übergang gedacht und gehören mit einem Enddatum versehen, sonst sind sie in fünf Jahren die nächste Altlast.
Wann Sie das Schema in Ruhe lassen
Nicht jede Altlast lohnt eine Behandlung, und eine Modernisierung, die beim Schema anfängt und dort auch endet, hat niemandem geholfen. Drei Fälle bleiben bei mir stehen.
Die Tabelle ist tot. Sie wird gelesen, nicht geschrieben, und nur von einem Bericht. Dann gehört sie nicht bereinigt, sondern in ein Archiv, und die Frage ist nicht mehr technisch, sondern eine der Aufbewahrung.
Die Altlast kostet nichts. Ein Wahrheitswert als 'J' und 'N' ist hässlich. Wenn er an drei Stellen gelesen wird und alle drei dieselbe Umrechnung benutzen, ist er ein Schönheitsfehler, kein Risiko. Wo derselbe Wert an zwanzig Stellen unterschiedlich ausgewertet wird, ist er ein Fehler, der auf seinen Tag wartet. Der Unterschied steckt nicht im Schema, sondern in der Zahl der Leser.
Das System hat ein Enddatum. Wenn die Plattform in achtzehn Monaten abgelöst wird, ist jede Schemaänderung Geld in einem Auslaufmodell. Wann das der richtige Schluss ist, steht in einem eigenen Artikel.
Bei einem Versionssprung der Datenbank kommen eigene Altlasten dazu: MySQL 5.7 auf 8.0.
Die Reihenfolge, die sich bewährt hat
Zum Schluss die Abfolge, in der ich vorgehe, wenn ein Schema ernsthaft im Weg steht.
- Zählen, nicht raten. Größe, letzte Schreibzugriffe, Benutzerliste. Eine halbe Stunde, und danach reden alle über dieselben Zahlen.
- Integrität herstellen, bevor Struktur geändert wird. Waisen finden, entscheiden, was mit ihnen passiert, und danach die Fremdschlüssel setzen, die fehlen. Das ist unspektakulär und verhindert die meisten späteren Überraschungen.
- Die eine Spalte angehen, die die meiste Verwirrung stiftet. Nicht alle, eine. Mit dem vollen fünfschrittigen Weg, inklusive Frist. Das ist der Durchlauf, an dem das Team das Verfahren lernt.
- Erst danach über Umzüge reden. Eine andere Datenbankversion, ein verwalteter Dienst, eine Aufteilung: das sind eigene Vorhaben, und sie werden deutlich einfacher, wenn die Integrität vorher stimmt.
Was aus dieser Liste am häufigsten fehlt, ist der zweite Punkt. Er hat keinen sichtbaren Nutzen, er verändert kein Verhalten, und er ist der Grund, warum die Schritte danach planbar werden. Wie eine schrittweise Modernisierung insgesamt abläuft, steht auf einer eigenen Seite.
Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.

