Eine neue Spalte, ein Index oder eine strengere Constraint wirken im Pull-Request harmlos – bis das Deployment im Peak-Traffic hängt, weil eine Migration unbemerkt lange Sperren hält oder ein Backfill die I/O-Limits sprengt. In PostgreSQL entstehen die meisten Ausfälle rund um Schemaänderungen nicht durch „falsches SQL“, sondern durch unklare Nebenwirkungen: Locks, lang laufende Transaktionen, Timing mit Applikations-Releases und fehlende Rückwege.
Ein belastbarer Ansatz orientiert sich an drei Zielen: Änderungen möglichst additiv gestalten, große Datenbewegungen in kontrollierbare Schritte zerlegen und jede Migration so bauen, dass ein Abbruch nicht zu inkonsistenten Zwischenständen führt. Damit wird „Null-Downtime“ kein Marketingbegriff, sondern eine Abfolge praktischer Entscheidungen.
Welche Schemaänderungen in PostgreSQL wirklich riskant sind
PostgreSQL nutzt MVCC (Multi-Version Concurrency Control). Viele DDL-Operationen sind schneller als erwartet, einige benötigen jedoch exklusive Locks, die konkurrierende Queries blockieren. Entscheidend ist nicht nur die Dauer der Operation, sondern auch: Wie lange warten andere Sessions auf den Lock? Und wie lange halten bestehende Transaktionen den alten Zustand „fest“?
Locks: Was blockiert wen – und warum das im Deployment zählt
DDL braucht in der Regel einen Lock auf der betroffenen Tabelle. Bei „kleinen“ Änderungen wirkt das unsichtbar, doch bei hoher Last gibt es fast immer mindestens eine lang laufende Transaktion (Report, Batch, Admin-Session). Diese kann DDL verzögern, und umgekehrt kann DDL Schreibtraffic aufstauen.
Typische Stolperstellen im Alltag:
- ALTER TABLE mit Operationen, die Daten umschreiben oder Rewrites auslösen.
- Änderungen an Spaltentypen oder DEFAULT-Werten mit impliziten Rewrites (je nach Version/Operation).
- Constraints, die bei der Validierung große Datenmengen scannen.
Praktische Konsequenz: Migrationen müssen so designt werden, dass sie im Worst Case abbrechen können, ohne die Applikation zu beschädigen – und dass sie die Lock-Zeit minimieren.
Lang laufende Transaktionen: Unsichtbare „Lock-Verlängerer“
Auch wenn eine DDL-Operation selbst schnell wäre, kann sie auf Locks warten. Gleichzeitig werden Vacuum/Autovacuum und das Aufräumen alter Tupel durch lange Transaktionen beeinträchtigt. In Deployment-Fenstern fällt das besonders auf, weil mehrere Komponenten zeitgleich aktiv sind (Deploy, Warmup, Background Jobs).
Empfehlung für den Betrieb: Vor Migrationen prüfen, ob ungewöhnlich alte Transaktionen laufen und ob die Applikation Pools sauber konfiguriert hat (Timeouts, Idle-in-transaction vermeiden). Das reduziert nicht nur Lock-Wartezeiten, sondern auch Nebenwirkungen wie bloat-bedingte Latenzspitzen.
Additive Änderungen: Schema erweitern, ohne die App zu brechen
Der stabilste Migrationsstil ist „erst erweitern, dann umschalten, dann aufräumen“. Damit bleibt die alte Applikationsversion kompatibel, während die neue Version bereits mit dem erweiterten Schema arbeiten kann. Dieses Vorgehen passt zu Blue/Green-Deployments, Rolling Updates und auch zu klassischen „Deploy + Smoke Test“-Abläufen.
Neue Spalten und Defaults: zuerst optional, später verpflichtend
Neue Spalten sollten initial NULL erlauben. Der Default wird zunächst nur in der Applikation gesetzt (beim Schreiben), nicht zwingend über die Datenbank. Das verhindert, dass bestehende Zeilen sofort umgeschrieben oder validiert werden müssen.
Ein bewährtes Muster:
- Spalte hinzufügen, NULL erlauben.
- Applikation schreibt ab sofort auch die neue Spalte.
- Backfill bestehender Zeilen in kleinen Batches.
- Constraint (NOT NULL oder CHECK) später nachziehen – idealerweise mit schrittweiser Validierung.
So wird eine potenziell „große“ Änderung in mehrere kleine, kontrollierbare Schritte zerlegt.
Index-Strategie: Online aufbauen, Writes nicht abwürgen
Indizes sind häufige Performance-Hebel, aber auch typische Migrationsrisiken. Der Indexaufbau kann viel I/O erzeugen und auf großen Tabellen lange dauern. In PostgreSQL lässt sich vieles im laufenden Betrieb erledigen, wenn der Aufbau so gewählt wird, dass er Schreibzugriffe nicht unnötig blockiert.
Entscheidend ist, dass die Applikation während des Indexaufbaus weiterhin korrekt funktioniert – auch dann, wenn der Index noch nicht existiert oder noch nicht genutzt wird. Gute Praxis ist daher, Queries zuerst „index-freundlich“ zu schreiben und den Index danach auszurollen.
Vertiefend zur Indexplanung passt: Datenbankindizes planen – schneller suchen ohne Schreibbremse.
Backfills ohne Stress: Daten in Batches nachziehen
Backfills (nachträgliches Befüllen neuer Felder) sind oft der Moment, in dem eine Migration „gefühlt“ zur Datenmigration wird. Das Problem ist selten die Logik, sondern die Last: große UPDATEs erzeugen WAL, belasten I/O, halten Zeilenversionen offen und können Autovacuum ausbremsen.
Chunking statt Big Bang: kleine Transaktionen, klare Progress-Metriken
Statt ein UPDATE über Millionen Zeilen auszuführen, wird in Batches gearbeitet. Damit bleiben Locks kurz, das System reagiert besser auf Traffic-Spitzen, und ein Abbruch ist unkritisch: Der nächste Lauf macht weiter.
Ein typischer Batch-Ansatz basiert auf einem stabilen Cursor (z. B. monoton steigender Primärschlüssel). Wichtig ist, dass die Batchgröße zur Umgebung passt: Eine Größe, die in Staging „gut aussieht“, kann in Produktion zu viel sein, wenn parallel viele Writes laufen.
Für das Scheduling eignen sich Background Worker oder Job-Queues. Dazu passt als Ergänzung: Async Jobs im Backend – Worker, Queues und Retries richtig.
Idempotenz: Backfill-Jobs müssen wiederholbar sein
Backfills sollten so implementiert werden, dass mehrere Runs nicht schaden. Das gelingt, wenn die Update-Operation nur Zeilen anfasst, die noch nicht migriert sind (z. B. „WHERE new_col IS NULL“ oder ein explizites Migrationsflag). Damit wird der Vorgang robust gegenüber Retries, Deploy-Rollbacks und kurzfristigen Datenkorrekturen.
Das Thema „wiederholbar ohne Nebenwirkungen“ hängt eng mit idempotenten APIs zusammen – das Prinzip ist identisch, nur auf Datenmigrationen übertragen.
Constraints und Datenqualität: erst messen, dann scharf schalten
Eine Constraint verbessert Datenqualität, kann aber beim Aktivieren Probleme verursachen, wenn Bestandsdaten nicht passen. Wer Constraints „hart“ ausrollt, findet Fehler im ungünstigsten Moment: während des Deployments.
Checks schrittweise ausrollen: von „beobachten“ zu „erzwingen“
Statt sofort eine neue Regel strikt zu erzwingen, lohnt sich ein zweistufiger Rollout:
- Regel in der Applikation implementieren (Writes validieren), um neue Verstöße zu verhindern.
- Bestehende Daten prüfen und bereinigen (Backfill/Repair).
- Constraint aktivieren, sobald die Datenlage sauber ist.
Bei CHECK-Constraints ist zudem wichtig, wie die Validierung erfolgt. Eine Validierung kann große Tabellen scannen und dadurch Last erzeugen. Deshalb sollte der Validierungsschritt bewusst terminiert und beobachtet werden.
Foreign Keys: Integrität erhöhen, ohne Bulk-Operationen zu blockieren
Foreign Keys sind hervorragend für Konsistenz, aber in Systemen mit großen Tabellen und Bulk-Jobs müssen sie bewusst eingeführt werden. Vor dem Rollout ist zu klären:
- Existiert ein passender Index auf den Referenzspalten?
- Wie werden Deletes/Updates gehandhabt (CASCADE, RESTRICT)?
- Gibt es Daten, die aktuell nicht referenziell sauber sind?
Ohne diese Vorarbeit drohen lange Validierungen, unerwartete Fehlerpfade in der Applikation oder Performanceeinbrüche bei Schreiblast.
Release-Abfolge: Datenbank und App sauber koordinieren
In der Praxis scheitern Migrationen häufig an der Kopplung zwischen Applikation und Datenbank. Eine neue App-Version erwartet ein neues Feld, während noch nicht alle Instanzen umgestellt sind. Oder ein Cleanup entfernt eine Spalte, die noch von einem alten Worker genutzt wird.
Expand/Contract als Standard: kompatible Übergänge erzwingen
Die sicherste Regel lautet: Erst erweitern, dann umstellen, dann reduzieren. Konkret heißt das:
- „Expand“: Schemaänderungen bereitstellen, die die alte App nicht stören.
- „Switch“: Applikation schreibt/liest die neuen Strukturen.
- „Contract“: Alte Spalten, alte Indizes, alte Trigger erst entfernen, wenn keine Nutzung mehr existiert.
Wichtig ist die Übergangsphase, in der beide Versionen der Applikation parallel korrekt funktionieren. Bei Rolling Updates ist diese Phase immer vorhanden; bei Blue/Green kann sie kurz sein, aber sie existiert ebenfalls (Warmup, Traffic-Shift, Background Jobs).
Rollback-fähig bleiben: Schema-Änderungen sind schwer rückgängig
Ein Rollback der Applikation ist meist schnell. Ein Rollback der Datenbankstruktur dagegen ist oft riskant, vor allem wenn Daten bereits in neue Spalten geschrieben wurden. Deshalb sollte jede Migration einen klaren Rückweg haben:
- Neue Daten parallel auch in alte Felder schreiben (temporär), bis Stabilität bestätigt ist.
- Cleanup erst in einem späteren Release durchführen.
- Bei kritischen Änderungen explizite „Stop-Points“ definieren (z. B. nach Expand und nach Backfill).
Das minimiert den Druck im Incident-Fall: Die Applikation kann zurückrollen, ohne dass Daten verloren gehen oder Schemas inkompatibel werden.
Ein praxistauglicher Ablauf für Migrationen im Team
Technik alleine reicht nicht; Migrationen sind ein Prozess. Hilfreich ist ein wiederholbares Vorgehen, das im Code Review, in CI und im Betrieb verankert ist. Dabei zählen klare Verantwortlichkeiten: Wer entscheidet über Batchgrößen? Wer überwacht die Laufzeit? Wer stoppt den Job bei Anomalien?
Konkrete Schritte, die sich in realen Deployments bewähren
- Migrationsskripte so schreiben, dass sie in kleinen Schritten laufen und bei Abbruch wiederholbar sind.
- Vor dem Merge prüfen: Welche Locks entstehen? Wird ein Table-Scan erzwungen? Ist ein Index notwendig?
- Backfills als separate Jobs planen (nicht „nebenbei“ in einer einzelnen Migration verstecken).
- Für jede Änderung definieren: Was ist der Rückweg, wenn die neue App-Version zurückgerollt werden muss?
- Produktionsnah testen: Laufzeit, I/O-Last und Query-Pläne in einer Umgebung mit ähnlicher Datenmenge validieren.
Zusammenspiel mit CI/CD und Observability
Migrationen gehören in die Pipeline, aber nicht jede Migration sollte automatisch „durchlaufen“, ohne Guardrails. Sinnvoll sind klare Timeouts und Abbruchkriterien. Zusätzlich hilft Telemetrie: Wenn während einer Migration die Latenz steigt, sollte das als Ereignis sichtbar sein und nicht erst über Support-Tickets auffallen.
Für den Betrieb sind strukturierte Logs und Metriken entscheidend. Passend dazu: OpenTelemetry Metrics – sinnvolles Monitoring statt Zahlenfriedhof.
Entscheidungshilfe: Welche Migrationsform passt zur Änderung?
Im Alltag ist weniger die perfekte Theorie gefragt als eine schnelle, robuste Einordnung. Die folgenden Leitfragen helfen, ohne lange Meetings die richtige Strategie zu wählen.
Wenn Daten umgeschrieben werden: Last kontrollieren statt hoffen
- Betreffen die Änderungen viele Zeilen? Dann Backfill als Batch-Job planen, nicht als riesiges UPDATE.
- Ist Schreibtraffic hoch? Dann Operationen bevorzugen, die kurze Locks halten, und Batchgrößen konservativ wählen.
- Gibt es harte SLAs auf Latenz? Dann Migrationen in kleine, stoppbare Schritte aufteilen und eng überwachen.
Wenn die App-Abhängigkeit hoch ist: Kompatibilität erzwingen
- Lesen alte und neue App-Versionen gleichzeitig? Dann nur Änderungen ausrollen, die beide tolerieren.
- Wird ein Feld entfernt? Dann erst Telemetrie/Logs nutzen, um Nicht-Nutzung zu bestätigen, und erst später löschen.
- Werden Regeln strenger? Dann zuerst Writes in der App validieren, dann Daten bereinigen, dann die Regel in der DB erzwingen.
Wer diese Prinzipien konsequent anwendet, baut Migrationen, die sich wie normale Releases anfühlen: planbar, beobachtbar und mit einem Rückweg. Genau daraus entsteht in der Praxis eine belastbare Null-Downtime-Migration – nicht durch „magische“ SQL-Tricks, sondern durch sauberes Engineering über Code, Daten und Betrieb hinweg.
