Artikel

Das Schema neu aufsetzen, wenn niemand hinsieht

Sechs Jahre Migrationsgeschichte, Reste aus drei Datenbanksystemen, Tabellen ohne Nutzer. Wie ich das Datenmodell auf einen sauberen Anfangszustand gebracht habe — ohne Daten zu verlieren.

BlackZackBlackzack

912 Wörter · 5 Min. Lesezeit

  • datenbank
  • yurna
  • wartung

Mein Datenmodell hatte eine Vorgeschichte: angefangen mit Dateien, dann eine Datenbank, dann eine andere, dann eine dritte. Dazwischen Migrationen, ein Content-System, das wieder verschwand, Features, die entfernt wurden, und Felder, die nach ihrer letzten Benutzung nie jemand angefasst hat.

Das Ergebnis war ein Schema, das funktionierte und das man nicht mehr lesen konnte.

Was sich angesammelt hatte

Tabellen ohne Nutzer. Reste des Content-Systems, Tabellen von entfernten Befehlen, eine Sammlung für ein Feature, das nie fertig wurde.

Felder mit unklarer Bedeutung. Ein type als Zeichenkette mit fünf möglichen Werten, von denen zwei seit Jahren nicht mehr vorkamen. Ein data-Feld mit JSON, dessen Inhalt sich über die Zeit dreimal geändert hatte.

Uneinheitliche Benennung. guild_id und guildId und guild. Zeitstempel mal als created, mal als created_at, mal als erstellt.

Zwei Wahrheiten für denselben Wert. Ein Zähler, der auch aus einer Tabelle berechenbar wäre, und der bei einem Fehler einmal auseinandergelaufen ist.

Fehlende Indizes, weil Abfragen dazukamen, ohne dass jemand nachgesehen hat.

Nichts davon war akut. Alles davon kostet bei jeder Änderung ein bisschen Zeit und ein bisschen Aufmerksamkeit.

Der Anlass

Der Umzug. Wenn ohnehin alles auf eine neue Maschine geht, ist der Moment günstig — und es gibt einen praktischen Grund: Die Migrationshistorie, die sich über die Jahre angesammelt hatte, beschrieb Schritte von einem Datenbanksystem zu einem anderen, das ich gar nicht mehr benutze. Ein Neuaufbau aus dieser Historie hätte gar nicht mehr funktioniert.

Der Weg: ein sauberer Anfangszustand

Der Ansatz war, den aktuellen Stand als neuen Ausgangspunkt zu definieren.

Schritt 1: Aufräumen im Schema selbst. Ungenutzte Tabellen und Felder raus, Benennung vereinheitlichen, Indizes ergänzen, berechenbare Werte entfernen.

Schritt 2: Eine einzige Migration erzeugen, die dieses Schema von null aufbaut. Alle alten Migrationsdateien archiviert, nicht gelöscht.

Schritt 3: Die vorhandene Datenbank auf den neuen Stand bringen. Nicht durch die neue Migration — die würde bei einer bestehenden Datenbank scheitern —, sondern durch die Änderungen, die sich aus Schritt 1 ergeben, einzeln und geprüft.

Schritt 4: Gegenprobe. Eine frisch aufgebaute Datenbank und die migrierte bestehende müssen strukturgleich sein. Das lässt sich vergleichen, indem man die Strukturbeschreibung beider ausliest und gegenüberstellt.

Schritt 4 ist der, den man nicht auslassen darf. Ohne ihn hat man zwei Wege zum selben Ziel und keine Sicherheit, dass sie dort ankommen.

Warnung

Bei den Löschungen gilt eine Reihenfolge, die ich mir hart erarbeitet habe: erst aus dem Code entfernen, dann eine Weile beobachten, dann aus der Datenbank entfernen. Zwischen beiden Schritten mindestens eine Woche Betrieb. Ein Feld, das noch irgendwo gelesen wird, macht sich sofort bemerkbar — und dann ist es ein Fehler im Code und kein Datenverlust.

Was ich dabei gefunden habe

Eine Tabelle mit 40.000 Zeilen, die nichts tat. Ereignisprotokolle eines Features, das ich vor zwei Jahren entfernt hatte. Die Tabelle wurde weiter befüllt, weil ein Ereignishandler stehen geblieben war, den niemand aufgerufen hat — außer dem System selbst.

Ein Index, der nie benutzt wurde, weil die Abfrage, für die er gebaut war, längst anders formuliert wird.

Zwei Felder, die dasselbe bedeuteten. Eines wurde vom Bot geschrieben, eines vom Dashboard. Beide beschrieben, wann ein Server zuletzt aktiv war, mit unterschiedlichen Definitionen von „aktiv".

Der letzte Punkt ist der interessanteste, weil er kein technischer Fehler ist, sondern ein fachlicher. Zwei Teile des Systems hatten unterschiedliche Vorstellungen von einem Begriff, und weil jeder sein eigenes Feld hatte, ist es nie aufgefallen.

Der Vergleich, mit dem ich sichergegangen bin

Der entscheidende Schritt beim Neuaufsetzen ist die Gegenprobe: Ist eine frisch erzeugte Datenbank strukturgleich mit der migrierten bestehenden?

Der Weg dafür ist unspektakulär: die Strukturbeschreibung beider auslesen, sortieren und vergleichen.

sqlite3 frisch.db .schema | sort > /tmp/frisch.txt
sqlite3 bestand.db .schema | sort > /tmp/bestand.txt
diff /tmp/frisch.txt /tmp/bestand.txt

Bei mir kamen dabei drei Unterschiede heraus, die ich alle nicht erwartet hatte: ein Index, den ich in der bestehenden Datenbank per Hand angelegt und nie ins Schema übernommen hatte; eine Tabelle aus einem alten Versuch; und eine abweichende Reihenfolge von Spalten, die entstanden war, weil ich sie nachträglich hinzugefügt hatte.

Die ersten beiden waren echte Funde. Der dritte ist harmlos und trotzdem gut zu wissen — eine nachträglich angefügte Spalte steht am Ende, auch wenn sie im Schema woanders steht.

Warum ich nichts gelöscht habe, ohne es vorher stillzulegen

Bei den Entfernungen habe ich eine Reihenfolge eingehalten, die aus Erfahrung stammt.

Erst aus dem Code entfernen. Ab jetzt schreibt und liest niemand mehr.

Dann eine Woche beobachten. Wenn doch irgendwo ein Zugriff steht, taucht er als Fehler auf — und das ist ein Codefehler, der behebbar ist.

Dann umbenennen statt löschen. Eine Tabelle mit einem Präfix wie alt_ versehen. Sie ist damit unsichtbar für die Anwendung, aber noch da.

Dann, Wochen später, löschen.

Der dritte Schritt ist der, den ich mir angewöhnt habe, nachdem ich einmal eine Tabelle gelöscht hatte, die von einem seltenen Vorgang gebraucht wurde — einem, der nur monatlich lief und deshalb in der Beobachtungswoche nicht vorkam.

Seitdem gilt: Die Beobachtungszeit muss länger sein als der längste Zyklus im System. Wer monatliche Aufgaben hat, kann nach einer Woche nichts ausschließen.

Was ich mitnehme

Ein Schema ist Dokumentation. Wenn man es liest und die Fachlichkeit nicht erkennt, ist es schlechte Dokumentation. Aufräumen ist deshalb kein Selbstzweck, sondern macht das System verständlich.

Aufräumen braucht einen Anlass. Ich habe das Schema sechs Jahre lang nicht angefasst, weil es keinen gab. Der Umzug war der Anlass, und ich glaube, dass solche Arbeiten fast immer an etwas anderes gekoppelt sind.

Namensdisziplin ist keine Kosmetik. guild_id und guildId nebeneinander sind eine dauerhafte kleine Fehlerquelle. Die Kosten fallen nicht beim Anlegen an, sondern bei jedem, der es später liest.