Spalten umbenennen, Tabellen aufteilen und Constraints ändern ohne Ausfallzeit

Sie haben eine users-Tabelle mit einer Spalte namens full_name. Das Team entscheidet, dass sie stattdessen display_name heißen soll. Wenn Sie sie direkt umbenennen, brechen alle Anwendungen, die noch full_name lesen, sobald die Änderung in Produktion geht. Die Spalte ist weg, die Queries schlagen fehl, und die Nutzer sehen Fehler.

Dies ist kein hypothetisches Problem. Teams benennen Spalten um, teilen Tabellen in zwei auf und ändern Constraints in jedem Sprint. Der naive Ansatz – Schema ändern und Code später fixen – verursacht Produktionsausfälle, die vermeidbar gewesen wären. Die Lösung ist ein Muster namens Expand-Contract, das für alle drei Szenarien funktioniert.

Die Kernidee: Erst hinzufügen, dann schrittweise umstellen, zuletzt entfernen

Das Expand-Contract-Muster besteht aus drei Phasen. Zuerst erweitern Sie das Schema, indem Sie neue Strukturen neben den alten hinzufügen. Dann migrieren Sie Anwendungen und Daten auf die neuen Strukturen. Schließlich ziehen Sie sich zurück, indem Sie die alten Strukturen entfernen, sobald nichts mehr davon abhängt.

Das folgende Diagramm veranschaulicht das dreiphasige Expand-Contract-Muster und seine Anwendung auf das Umbenennen einer Spalte, das Aufteilen einer Tabelle und das Ändern eines Constraints.

flowchart TD A[Start] --> B[Phase 1: Expand] B --> C[Neue Spalte/Tabelle/Constraint hinzufügen] C --> D[In alte und neue Strukturen schreiben] D --> E[Phase 2: Migrieren] E --> F[Vorhandene Daten nachziehen] F --> G[Anwendungen auf neue Struktur umstellen] G --> H{Alle Konsumenten migriert?} H -- Nein --> G H -- Ja --> I[Phase 3: Contract] I --> J[Abhängigkeiten von alter Struktur prüfen] J --> K[Alte Spalte/Tabelle/Constraint löschen] K --> L[Fertig]

Der entscheidende Punkt ist, dass Sie nie einen Bruch in einem Schritt verursachen. Sie halten den alten Pfad immer funktionsfähig, bis der neue vollständig übernommen ist. Das bedeutet null Ausfallzeit bei Schemaänderungen, solange Sie die Reihenfolge korrekt einhalten.

Eine Spalte umbenennen, ohne etwas zu beschädigen

Gehen wir das Umbenennen von full_name zu display_name durch. In der Expand-Phase fügen Sie eine neue Spalte display_name zur users-Tabelle hinzu. Sie löschen full_name nicht. Die neue Version Ihrer Anwendung beginnt, in beide Spalten zu schreiben. Jeder INSERT oder UPDATE schreibt denselben Wert in full_name für alte Konsumenten und in display_name für neue Konsumenten.

Hier sind die SQL-Befehle für jede Phase der Umbenennung:

-- Phase 1: Expand – neue Spalte hinzufügen
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);

-- Beispiel für Dual-Write (Anwendungslogik, nicht nur SQL)
-- Beim Einfügen oder Aktualisieren eines Benutzers in beide Spalten schreiben:
INSERT INTO users (full_name, display_name) VALUES ('Alice', 'Alice');
UPDATE users SET full_name = 'Bob', display_name = 'Bob' WHERE id = 42;

-- Phase 2: Migrieren – vorhandene Daten nachziehen
UPDATE users SET display_name = full_name WHERE display_name IS NULL;

-- Phase 3: Contract – alte Spalte löschen
ALTER TABLE users DROP COLUMN full_name;

Diese Abfolge stellt sicher, dass die Datenbank zu keinem Zeitpunkt eine Abfrage ablehnt oder Daten verliert.

Nachdem die Spalte existiert und die Anwendung in beide schreibt, führen Sie ein Backfill durch. Dies ist ein Batch-Prozess, der alle vorhandenen Werte aus full_name in display_name für jede Zeile kopiert. Sie überprüfen, ob die Anzahl übereinstimmt, und stichprobenartig einzelne Datensätze, um sicherzustellen, dass nichts verloren gegangen ist.

Jetzt kommt der Wechsel. Alle Anwendungen, die Benutzernamen lesen, müssen aktualisiert werden, um display_name statt full_name zu lesen. Dies kann schrittweise erfolgen. Einige Dienste wechseln zuerst, andere folgen. Während dieser Zeit bleiben beide Spalten gefüllt, sodass jeder Dienst, der noch full_name liest, weiterhin funktioniert.

Sobald jede Anwendung und jede Abfrage migriert ist, treten Sie in die Contract-Phase ein. Sie löschen die Spalte full_name. Der gesamte Prozess dauert Tage oder Wochen, je nachdem, wie viele Dienste aktualisiert werden müssen, aber es gibt nie einen Moment, in dem Benutzer nicht auf die Anwendung zugreifen können.

Eine Tabelle in zwei aufteilen

Dieses Szenario ist komplexer. Stellen Sie sich vor, Ihre orders-Tabelle speichert Bestelldetails und Zahlungsinformationen in derselben Zeile. Das Team möchte Zahlungsdaten in eine dedizierte payments-Tabelle auslagern. Sie können nicht einfach die neue Tabelle erstellen und aufhören, in die alte zu schreiben, weil bestehende Anwendungen immer noch aus orders lesen.

Die Expand-Phase erstellt die payments-Tabelle. Die neue Version der Anwendung beginnt, Zahlungsdaten an beide Stellen zu schreiben. Jedes Mal, wenn eine Bestellung erstellt oder aktualisiert wird, schreibt die Anwendung die Zahlungsdetails in die orders-Tabelle für alte Konsumenten und in die payments-Tabelle für die neue Struktur. Dies wird als Dual-Write bezeichnet und ist der schwierigste Teil, den es richtig zu implementieren gilt. Beide Schreibvorgänge müssen erfolgreich sein oder beide müssen zurückgesetzt werden. Teilweise Schreibvorgänge beschädigen Ihre Daten.

Backfill ist hier entscheidend. Sie müssen alle vorhandenen Zahlungsdaten aus orders in payments kopieren. Führen Sie dies in Batches durch, um die Tabelle nicht zu lange zu sperren. Überprüfen Sie nach jedem Batch, ob die Anzahl der Datensätze und die Gesamtzahlungsbeträge zwischen den beiden Tabellen übereinstimmen. Wenn sie nicht übereinstimmen, stoppen Sie und untersuchen Sie den Fehler, bevor Sie fortfahren.

Sobald das Backfill verifiziert ist und alle Anwendungen aktualisiert wurden, um Zahlungsdaten aus payments statt aus orders zu lesen, treten Sie in die Contract-Phase ein. Sie entfernen die Zahlungsspalten aus orders. Dieser Schritt erfordert Vertrauen, dass keine Abfrage, kein Bericht und kein Legacy-Dienst mehr auf diese Spalten zugreift. Überprüfen Sie Ihre Datenbank-Logs, Anwendungs-Logs und das Query-Monitoring, bevor Sie etwas löschen.

Einen Nullable-Constraint in Not Null ändern

Dieses Szenario sieht einfach aus, überrascht aber oft Teams. Ihre users-Tabelle hat eine email-Spalte, die Null-Werte erlaubt. Das Geschäft verlangt nun, dass jeder Benutzer eine E-Mail-Adresse hat. Wenn Sie die Spalte direkt auf NOT NULL ändern, wird die Datenbank die Änderung ablehnen, weil vorhandene Zeilen mit Null-E-Mails den Constraint verletzen.

Die Expand-Phase fügt hier keine neue Spalte im traditionellen Sinne hinzu. Ein üblicher Ansatz ist das Hinzufügen einer neuen Spalte email_not_null, die email spiegelt, aber mit einem Not-Null-Constraint. Die neue Anwendungsversion schreibt in beide Spalten. Bei Einfügungen erhalten beide Spalten denselben Wert. Bei Aktualisierungen werden beide aktualisiert.

Backfill ist der entscheidende Schritt. Jede Zeile mit einer Null-E-Mail muss korrigiert werden. Sie müssen entweder einen Standardwert bereitstellen, mit Benutzern koordinieren, dass sie ihre E-Mail-Adresse eingeben, oder mit anderen Teams zusammenarbeiten, um die fehlenden Daten zu liefern. Dies ist nicht nur ein technisches Problem. Es ist ein Datenqualitäts- und Organisationsproblem. Das Backfill-Skript sollte jede Zeile protokollieren, die es nicht korrigieren kann, und das Team alarmieren, um diese Fälle manuell zu behandeln.

Nachdem alle Zeilen gültige E-Mail-Adressen haben und alle Anwendungen aus email_not_null lesen, ziehen Sie sich zurück, indem Sie die alte Spalte email löschen. Wenn Sie den ursprünglichen Spaltennamen behalten möchten, können Sie email_not_null in email umbenennen, nachdem die alte Spalte entfernt wurde.

Praktische Checkliste für Schemaänderungen

Bevor Sie eine Schema-Migration in Produktion durchführen, gehen Sie diese Checkliste durch:

  • Kann das alte Schema nach der Änderung noch Anfragen bedienen?
  • Gibt es einen Dual-Write-Pfad für alle neuen Daten?
  • Ist das Backfill-Skript auf einer Kopie der Produktionsdaten getestet?
  • Haben Sie überprüft, ob alle Konsumenten migriert sind, bevor Sie etwas löschen?
  • Haben Sie einen Rollback-Plan, falls die Contract-Phase eine übersehene Abhängigkeit offenbart?

Das Fazit

Jede Schemaänderung, die Sie in Produktion vornehmen, sollte derselben Abfolge folgen: neue Struktur hinzufügen, Daten und Anwendungen schrittweise migrieren und die alte Struktur nur entfernen, wenn nichts mehr davon abhängt. Ob Sie eine Spalte umbenennen, eine Tabelle aufteilen oder einen Constraint verschärfen – das Muster ist dasselbe. Der Preis ist Geduld und sorgfältige Koordination. Die Belohnung ist null Ausfallzeit und null kaputte Abfragen.