Das Dilemma
Die Datenbank ist das Herz jeder Enterprise-Anwendung. Sie muss sich weiterentwickeln — neue Spalten, geänderte Constraints, neue Tabellen. Aber sie darf dabei nicht stehen bleiben. In einer Welt, die 24/7-Verfügbarkeit erwartet, ist „kurz offline für ein Update” keine Option mehr.
Warum naive Migrationen scheitern
Das Lock-Problem
Ein simples ALTER TABLE users ADD COLUMN phone VARCHAR(50) klingt harmlos. Bei PostgreSQL ist es das auch — ein leichtgewichtiger Vorgang. Aber ALTER TABLE orders ADD COLUMN total DECIMAL NOT NULL DEFAULT 0 bei einer Tabelle mit 50 Millionen Zeilen? Das kann Minuten dauern und hält in älteren MySQL-Versionen einen exklusiven Table Lock.
Das Kompatibilitätsproblem
Stellen Sie sich vor: Sie benennen eine Spalte um (ALTER TABLE users RENAME COLUMN name TO full_name). Der alte Anwendungscode sucht nach name. Die neuen Server suchen nach full_name. Während des Deployments laufen beide Versionen gleichzeitig. Ergebnis: Fehler.
Die Expand-Contract-Methode
Das bewährteste Pattern für Zero-Downtime-Migrationen besteht aus drei Phasen:
Phase 1: Expand (Erweitern)
Fügen Sie Neues hinzu, ohne Altes zu entfernen:
- Neue Spalte anlegen (nullable, ohne NOT NULL)
- Neue Tabelle erstellen
- Neuen Index erstellen (CONCURRENTLY bei PostgreSQL)
Die alte Anwendung läuft unverändert weiter.
Phase 2: Migrate (Daten überführen)
Daten in die neue Struktur überführen:
- Backfill in Batches (nicht alles auf einmal)
- Dual-Write: neue Anwendungsversion schreibt in alte UND neue Struktur
- Validierung: stimmen alte und neue Daten überein?
Phase 3: Contract (Bereinigen)
Erst wenn alle Anwendungsinstanzen die neue Struktur nutzen:
- Alte Spalten/Tabellen entfernen
- NOT NULL Constraints nachträglich setzen
- Alte Indizes entfernen
Konkrete Patterns
Spalte hinzufügen (sicher)
-- Phase 1: Nullable Spalte hinzufügen
ALTER TABLE users ADD COLUMN phone VARCHAR(50);
-- Phase 2: Backfill in Batches
UPDATE users SET phone = '' WHERE phone IS NULL AND id BETWEEN 1 AND 10000;
-- ... weitere Batches
-- Phase 3: Constraint setzen (nach vollständigem Backfill)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
Spalte umbenennen (sicher)
-- Phase 1: Neue Spalte anlegen
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
-- Phase 2: Dual-Write + Backfill
-- Anwendung schreibt in name UND full_name
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- Phase 3: Alte Spalte entfernen (nach vollständigem Deployment)
ALTER TABLE users DROP COLUMN name;
Index erstellen (sicher)
-- CONCURRENTLY verhindert Table Locks
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
Anti-Patterns
| Anti-Pattern | Risiko | Alternative |
|---|---|---|
NOT NULL bei neuer Spalte sofort setzen | Lock bei großen Tabellen | Nullable anlegen, dann nachträglich constrainen |
| Spalte direkt umbenennen | Inkompatibilität mit altem Code | Expand-Contract |
Große Tabelle in einem UPDATE backfillen | Lange Transaktion, Lock, Replication Lag | Batch-Updates mit Pausen |
| Migration und Deployment koppeln | Rollback wird unmöglich | Migration unabhängig vom Deployment |
Index ohne CONCURRENTLY | Table Lock während Index-Erstellung | CREATE INDEX CONCURRENTLY |
Tooling
Bewährte Werkzeuge für sichere Migrationen:
- gh-ost (GitHub): Online-Schema-Änderungen für MySQL ohne Locks
- pg-osc: Äquivalent für PostgreSQL
- Flyway / Liquibase: Versionierte Migrationsskripte mit Rollback-Unterstützung
- pgroll: Neues Tool von Xata für reversible PostgreSQL-Migrationen
- strong_migrations (Rails): Linter, der unsichere Migrationen blockiert
Fazit
Zero-Downtime-Migrationen sind kein Luxus, sondern eine Notwendigkeit für jede Anwendung, die Verfügbarkeit verspricht. Das Expand-Contract-Pattern ist einfach zu verstehen, aber es erfordert Disziplin: jede Schema-Änderung wird in mindestens zwei Schritte aufgeteilt, und zwischen den Schritten liegt ein vollständiges Deployment. Langsamer? Ja. Sicherer? Deutlich.