Datenbank-Migrationen schreiben, die die Produktion nicht lahmlegen
Sie haben eine Datenbank, die seit Monaten in Betrieb ist. Benutzer sind darauf angewiesen. Tabellen sind groß geworden, Abfragen wurden optimiert, und das Schema hat sich in eine Form eingependelt, die funktioniert. Dann kommt die Feature-Anfrage, die eine neue Spalte, eine Tabellenumbenennung oder eine Datenmigration erfordert.
In dem Moment, in dem Sie dieses ALTER TABLE in der Produktion ausführen, gehen Sie eine Wette ein. Wenn die Migration zu lange dauert, stauen sich die Abfragen. Wenn sie die Tabelle sperrt, sehen Benutzer Fehler. Wenn sie auf halbem Weg fehlschlägt, brauchen Sie einen Rückweg. Und wenn Sie keinen Rollback-Plan haben, bleibt nur die Wiederherstellung aus einem Backup – was bedeutet, dass alle seit dem letzten Snapshot eingegebenen Daten verloren gehen.
Deshalb geht es bei sicheren Datenbank-Migrationen nicht nur darum, korrektes SQL zu schreiben. Es geht darum, Änderungen so zu strukturieren, dass sie überprüft, getestet und ohne Panik rückgängig gemacht werden können.
Jede Migration braucht zwei Dateien
Das einfachste Muster, das Teams immer wieder rettet, ist das Paar aus Up- und Down-Migration.
Hier ist ein konkretes Beispiel für ein Paar aus Up- und Down-Migration:
-- 20241101_add_last_login_at.up.sql
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
-- 20241101_add_last_login_at.down.sql
ALTER TABLE users DROP COLUMN last_login_at;
Eine Up-Migration enthält das SQL, das die Änderung vornimmt. Eine Down-Migration enthält das SQL, das sie rückgängig macht. Jede Änderung bekommt beide Dateien, die zusammen mit einer eindeutigen Kennung gespeichert werden, damit die Reihenfolge klar ist.
20241101_add_email_index.sql -- up
20241101_add_email_index_down.sql -- down
Die Kennung kann ein Zeitstempel, eine Sequenznummer oder ein Datumspräfix sein. Wichtig ist, dass jeder, der in den Ordner schaut, die genaue Reihenfolge der Änderungen sehen kann. Wenn eine Migration in der Produktion läuft und etwas schiefgeht, gibt Ihnen die Down-Migration eine schnelle, vorhersagbare Möglichkeit, zurückzusetzen.
Ohne eine Down-Migration ist Ihr einziger Fallback eine vollständige Datenbankwiederherstellung. Das dauert, erfordert Koordination und riskiert Datenverlust. Eine Down-Migration läuft in Sekunden.
Wenn Down-Migrationen nicht ausreichen
Down-Migrationen funktionieren gut für reversible Änderungen: eine Spalte hinzufügen, einen Index erstellen, Referenzdaten einfügen. Aber einige Änderungen sind schwer rückgängig zu machen.
Das Löschen einer Spalte ist ein häufiges Beispiel. Sobald die Spalte weg und die Daten gelöscht sind, kann eine Down-Migration, die die Spalte wieder hinzufügt, die Daten nicht zurückbringen. Das gleiche Problem gilt für das Umbenennen von Tabellen, das Ändern von Spaltentypen oder das Entfernen von Constraints, von denen andere Teile des Systems abhängen.
Für diese Fälle besteht der sichere Ansatz darin, die Änderung in mehrere kleine Migrationen aufzuteilen, die jeweils für sich allein reversibel sind:
- Fügen Sie eine neue Spalte mit dem gewünschten Typ hinzu.
- Befüllen Sie die Daten in Batches nach.
- Aktualisieren Sie den Anwendungscode, um die neue Spalte zu verwenden.
- Entfernen Sie die alte Spalte.
Jeder Schritt hat seine eigene Up- und Down-Migration. Wenn Schritt 3 ein Problem offenbart, können Sie Schritt 2 und Schritt 1 sauber rückgängig machen. Sie erreichen nie einen Punkt, an dem der einzige Ausweg eine Wiederherstellung ist.
Schreiben Sie Migrationen, die mehrmals ausgeführt werden können
Pipelines fallen aus. Netzwerkabbrüche, Timeouts passieren, und manchmal läuft eine Migration nur zur Hälfte, bevor der Prozess abstürzt. Wenn die Pipeline es erneut versucht, läuft die Migration erneut.
Wenn Ihre Migration annimmt, dass die Änderung noch nicht angewendet wurde, wird sie beim zweiten Durchlauf fehlschlagen. Dieser Fehler blockiert die gesamte Pipeline und erfordert manuelles Eingreifen.
Machen Sie jede Migration idempotent. Verwenden Sie IF NOT EXISTS beim Erstellen von Tabellen oder Indizes. Verwenden Sie IF EXISTS beim Löschen von Objekten. Prüfen Sie, ob eine Spalte bereits existiert, bevor Sie sie ändern. Das Ziel ist einfach: Die gleiche Migration zweimal auszuführen sollte das gleiche Ergebnis liefern wie einmaliges Ausführen.
Vermeiden Sie lange Sperren auf großen Tabellen
Ein ALTER TABLE, das einen Spaltentyp ändert, kann die gesamte Tabelle für Minuten sperren, wenn sie Millionen von Zeilen hat. Während dieser Zeit wartet jeder Lese- und Schreibzugriff auf diese Tabelle. Benutzer sehen Timeouts. Warteschlangen bauen sich auf.
Die Lösung besteht darin, einstufige Schemaänderungen auf großen Tabellen zu vermeiden. Verwenden Sie stattdessen einen mehrstufigen Ansatz:
- Fügen Sie eine neue Spalte mit dem gewünschten Typ hinzu.
- Aktualisieren Sie Zeilen in Batches, um die neue Spalte zu befüllen.
- Fügen Sie bei Bedarf einen Index hinzu.
- Entfernen Sie die alte Spalte in einer späteren Migration.
Jeder Schritt sperrt kurz. Die Anwendung kann zwischen den Schritten weiterlaufen. Dieses Muster ist langsamer zu schreiben, aber viel sicherer auszuführen.
Halten Sie umgebungsspezifische Werte aus Migrationsdateien heraus
Eine Migrationsdatei sollte in Entwicklung, Staging und Produktion gleich funktionieren. Wenn Sie einen Datenbanknamen, ein Passwort oder eine Verbindungszeichenfolge fest in das SQL codieren, wird die Datei an eine Umgebung gebunden. Sie können sie nicht woanders ausführen, ohne sie zu bearbeiten, und das Bearbeiten einer Migrationsdatei nach der Überprüfung macht den Zweck der Versionskontrolle zunichte.
Verwenden Sie Parameter, Umgebungsvariablen oder Konfiguration, die das Migrationstool bereitstellt. Das SQL selbst sollte nur Schema- und Datenlogik enthalten, keine Umgebungsdetails.
Speichern Sie Migrationen zusammen mit dem Anwendungscode
Es gibt zwei gängige Ansätze: Migrationsdateien im selben Repository wie den Anwendungscode zu behalten oder sie in einem separaten Repository zu speichern. Beides funktioniert, aber die Wahl beeinflusst, wie Teams Änderungen koordinieren.
Wenn Migrationen im selben Repository leben, enthält jeder Pull-Request, der das Schema ändert, auch die Migration. Die Code-Review umfasst sowohl die Anwendungsänderung als auch die Datenbankänderung gemeinsam. Das macht es einfacher, Inkonsistenzen zu erkennen, wie eine Abfrage, die eine Spalte referenziert, die noch nicht hinzugefügt wurde.
Wenn Migrationen in einem separaten Repository leben, können sich die Anwendungs- und Datenbankänderungen in unterschiedlichen Zeitplänen entwickeln. Das ist nützlich, wenn mehrere Dienste eine Datenbank gemeinsam nutzen, erfordert aber mehr Koordination, um Schema und Code synchron zu halten.
In beiden Fällen ist der Schlüssel, dass Migrationsdateien versioniert, überprüft und nachvollziehbar sind. Eine Datenbankänderung sollte die gleiche Art von Audit-Trail hinterlassen wie eine Codeänderung.
Praktische Checkliste für das Schreiben sicherer Migrationen
Bevor Sie eine Migration in die Pipeline einspielen, gehen Sie diese Prüfpunkte durch:
- Hat jede Migration eine entsprechende Down-Migration?
- Kann die Down-Migration den vorherigen Zustand tatsächlich ohne Datenverlust wiederherstellen?
- Ist die Migration idempotent? Kann sie zweimal ohne Fehler ausgeführt werden?
- Wird die Migration eine große Tabelle für mehr als ein paar Sekunden sperren?
- Sind umgebungsspezifische Werte in der SQL-Datei nicht vorhanden?
- Ist die Migrationsdatei in einem Repository mit dem Anwendungscode oder einem dedizierten Datenbank-Repository gespeichert?
Das Fazit
Sichere Datenbank-Migrationen bedeuten nicht, Änderungen zu vermeiden. Sie bedeuten, Änderungen reversibel, testbar und überprüfbar zu machen. Jede Migrationsdatei, die Sie schreiben, ist ein kleiner Vertrag: Hier ist, was sich ändert, und hier ist, wie man es rückgängig macht. Wenn dieser Vertrag klar ist, kann das Team schneller vorankommen, weil es weiß, dass es einen Rückweg gibt.