Zero-Downtime-Datenbankmigrationen
Wie eine produktive Datenbank sich weiterentwickelt, ohne das laufende System auch nur für eine Sekunde anzuhalten. Framework- und datenbankneutral. Für CTOs, Architekten, Tech Leads und Senior Backend Engineers.
Was ist das? · Reference Guide
Ein belastbarer Leitfaden zu einer Engineering-Frage — mit Trade-offs, Kosten und dem Fall, in dem wir anders entscheiden. Kein Meinungsstück, sondern ein Referenztext. Zur Übersicht
- Autor
- Batunet Engineering
- Lesezeit
- 14 Min.
- Niveau
- Vertiefung
- Status
- Freigegeben
- Zuletzt geprüft
- 21. Juli 2026
- Aktualisiert
- 21. Juli 2026
Auf dieser Seite
Von allen Teilen eines Systems ist die Datenbank das unversöhnlichste. Man kann Anwendungscode ausrollen und wieder zurückrollen, einen Dienst neu starten, eine Version verwerfen — die Datenbank aber hält den Zustand, den man nicht neu erzeugen kann, sie überlebt jede einzelne Version des Codes, und alle laufenden Instanzen teilen sie sich zur selben Zeit. Ein Fehler im Code trifft eine Anfrage; ein Fehler an der Datenbank trifft alle und lässt sich oft nicht rückgängig machen.
Genau deshalb wird die Datenbankänderung so gefürchtet, dass viele Teams sie in ein Wartungsfenster verbannen: System anhalten, ändern, wieder anfahren. Das ist ehrlich, aber es skaliert nicht — je wichtiger das System, desto teurer jede Minute Stillstand. Dieser Text zeigt den anderen Weg: wie ein Schema sich unter Volllast weiterentwickelt, ohne das System anzuhalten. Die Muster gelten framework- und datenbankübergreifend; welche einzelne Operation wie stark sperrt, hängt vom jeweiligen System ab und gehört dort nachgeschlagen. Das Prinzip bleibt gleich.
Warum Datenbankänderungen gefährlich sind
Drei Eigenschaften machen die Datenbank gefährlich, und sie wirken zusammen.
Die erste ist die gemeinsame Nutzung unter mehreren Versionen. Bei einer rollierenden Auslieferung läuft für eine Zeitspanne die alte und die neue Version des Codes gleichzeitig gegen dasselbe Schema. Ein Schema, das nur zur neuen Version passt, bricht die alte, die noch Anfragen bedient — und umgekehrt. Jede Änderung muss deshalb in jedem Moment zu beiden laufenden Versionen passen. Ein Beispiel: Benennt die neue Version eine Spalte um, sucht die alte Version sie im selben Moment noch unter dem alten Namen — und jede ihrer Anfragen scheitert, bis die letzte alte Instanz ersetzt ist. Der Fehler liegt nicht im Code, sondern im Zusammentreffen zweier Versionen an einem Schema, das nur zu einer passt.
Die zweite ist das Sperrverhalten. Manche strukturellen Änderungen nehmen eine Sperre, die Lese- oder Schreibzugriffe auf eine Tabelle blockiert, während sie laufen. Auf einer kleinen Tabelle ist das unsichtbar; auf einer großen kann dieselbe Operation Minuten dauern und in dieser Zeit das ganze System zum Stillstand bringen — ein Ausfall ohne einen einzigen logischen Fehler, nur durch Dauer und Sperre.
Die dritte ist die Unumkehrbarkeit. Code kann man zurückrollen; gelöschte Daten nicht. Eine zerstörende Änderung — eine entfernte Spalte, eine gelöschte Tabelle — ist eine Einbahnstraße. Man kann vorwärts korrigieren, aber nicht zurück.
Zero-Downtime-Migration ist die Disziplin, alle drei zu umgehen: kompatibel zu bleiben, während zwei Versionen laufen; Sperren kurz und Operationen nebenläufig zu halten; und das Zerstörende so lange aufzuschieben, bis es sicher ist.
Expand / Contract
Das tragende Muster ist immer dasselbe: Man ändert nie im Bestand, sondern fügt das Neue hinzu, zieht darauf um und entfernt das Alte — in getrennten, einzeln auslieferbaren, rückwärtskompatiblen Schritten.
Expand erweitert das Schema additiv: eine neue Spalte, eine neue Tabelle, ohne dass etwas verschwindet. Migrate füllt die Lücke: Man schreibt eine Zeit lang in Alt und Neu zugleich und füllt die Bestandsdaten im Hintergrund nach. Contract zieht zusammen: Erst wenn nichts mehr das Alte liest oder schreibt, wird es entfernt. Jeder dieser Schritte ist für sich rückwärtskompatibel — und damit einzeln auslieferbar und einzeln umkehrbar, bis auf den letzten.
Schema: Kein Feld wird geändert; es wird hinzugefügt, gefüllt, dann das alte entfernt.
Ein konkretes Beispiel — eine Spalte name soll in voller_name umbenannt werden, was in einem Schritt die alte Version bräche. Expand: Man fügt die neue Spalte voller_name additiv hinzu, ohne name anzutasten. Migrate: Der Code schreibt fortan in beide Spalten, und ein Hintergrundjob kopiert die Bestandswerte von name nach voller_name. Ist alles gefüllt und geprüft, stellt der Code das Lesen auf voller_name um — bis hierhin lässt sich jeder Schritt zurückrollen. Contract: Wenn keine Version mehr name liest oder schreibt, wird die alte Spalte entfernt. Aus einer gefährlichen Umbenennung sind fünf harmlose Schritte geworden.
Die Empfehlung: Zerlegen Sie jede brechende Änderung in Expand, Migrate und Contract, statt sie in einem Schritt zu machen. Der Preis ist, dass aus einer Änderung mehrere Auslieferungen über Tage werden und eine Weile zwei Zustände nebeneinander gepflegt werden müssen. Anders entscheiden wir bei einem System mit akzeptablem Wartungsfenster und kleiner Datenmenge; dort ist die einmalige Änderung im Fenster einfacher und billiger als der mehrstufige Tanz.
Rückwärtskompatible Schema-Evolution
Die Regel hinter Expand/Contract lässt sich präzise fassen: Jeder Schema-Zustand muss zur gerade laufenden Codeversion und zur nächsten passen. Solange das gilt, kann man jederzeit ausliefern und zurückrollen, ohne die Datenbank anzufassen.
Ob eine einzelne Operation diese Regel in einem Schritt einhält, hängt von ihrer Art ab — und je nach Datenbank vom Detail:
| Operation | In einem Schritt sicher? | Die Falle | Sicherer Weg |
|---|---|---|---|
| Nullable Spalte hinzufügen | meist ja | — | direkt |
| Tabelle hinzufügen | ja | — | direkt |
| Index anlegen | oft nein | kann die Tabelle sperren | nebenläufig / online anlegen |
| Spalte mit NOT NULL und Default | oft nein | Umschreiben und Sperre auf großer Tabelle | nullable hinzufügen, füllen, dann Constraint |
| Constraint hinzufügen | oft nein | prüft alle Bestandsdaten, sperrt | erst „nicht validiert", dann separat validieren |
| Spalte umbenennen | nein | bricht die alte Version | Expand/Contract (neue Spalte, umziehen) |
| Spaltentyp ändern | nein | Umschreiben, brechend | neue Spalte, Backfill, Umstellung |
| Spalte entfernen | nein | bricht die noch laufende alte Version | Contract zuletzt, wenn niemand mehr liest |
Die additive Hälfte ist fast immer harmlos; die entfernende und die verändernde sind es nie. Jede Umbenennung, jede Typänderung, jedes Entfernen wird zu einer Kette aus additivem Schritt, Umzug und spätem Aufräumen.
Zwei der sicheren Wege verdienen eine Erklärung, weil sie oft überraschen. Ein Index lässt sich in vielen Datenbanken nebenläufig anlegen, ohne die Tabelle für Schreibzugriffe zu sperren — langsamer als der sperrende Weg, aber ohne Stillstand. Und ein Constraint lässt sich in zwei Phasen einführen: zuerst nur für neue Zeilen wirksam, ohne die Bestandsdaten sofort zu prüfen, und danach in einem separaten, nicht sperrenden Schritt validieren. Beide Male zerlegt man eine sperrende Operation in eine unsichtbare — das genaue Vorgehen unterscheidet sich je nach Datenbank, das Muster nicht.
Die Empfehlung: Erlauben Sie im Schema nur Zustände, die zur laufenden und zur nächsten Codeversion passen, und zerlegen Sie alles andere. Der Preis ist Vorausdenken bei jeder Änderung — man plant zwei bis drei Schritte, wo naiv einer stünde. Anders entscheiden wir bei einem internen Werkzeug, das man kurz anhalten darf; dort wiegt die Einfachheit der Ein-Schritt-Änderung schwerer als die Vermeidung einer Minute Stillstand.
Die Deploy-Sequenz
Die Kompatibilität entsteht nicht von allein, sondern aus der Reihenfolge, in der Schema- und Code-Auslieferungen ineinandergreifen. Die Invariante ist einfach: Das Schema wird vor dem Code erweitert, der es braucht, und nach dem Code aufgeräumt, der es nicht mehr benutzt. Dazwischen laufen die Schritte in fester Ordnung.
Im Einzelnen: Zuerst wird das Schema erweitert (Schritt 1). Dann liefert man Code aus, der in Alt und Neu zugleich schreibt und, wo vorhanden, aus dem Neuen liest (Schritt 2). Nebenläufig füllt der Backfill die Bestandsdaten (Schritt 3). Erst danach liefert man Code aus, der nur noch das Neue liest (Schritt 4). Und ganz zuletzt, wenn keine laufende Instanz mehr das Alte berührt, wird es entfernt (Schritt 5). Kein Schritt setzt voraus, dass ein anderer schon fertig ausgerollt ist — jeder ist für sich vollständig und verträglich.
Schema: Erst erweitern, dann umziehen, zuletzt entfernen — dazwischen ist jeder Zustand mit beiden Versionen verträglich.
Nie werden eine Schemaänderung und ihre brechende Nutzung in dieselbe Auslieferung gepackt. Jede App-Auslieferung ist rollierend, und in jedem Moment findet jede laufende Instanz ein Schema vor, das zu ihr passt.
Die Empfehlung: Liefern Sie Schema und Code getrennt und in dieser Ordnung aus — Expand vor Nutzung, Contract nach Ablösung. Der Preis ist ein längerer Auslieferungszyklus mit mehreren koordinierten Schritten statt einer einzigen Freigabe. Anders entscheiden wir bei einer rein additiven Änderung ohne Ablösung — eine neue Tabelle für eine neue Funktion; dort genügt ein Schritt, weil nichts Bestehendes umgestellt wird.
Feature-Flags und Migrationen
Die Auslieferung des Codes und das Aktivieren des neuen Verhaltens sind zwei verschiedene Dinge — und ein Feature-Flag trennt sie. Schema und Backfill können längst stehen, während das neue Verhalten hinter einem Flag noch ausgeschaltet ist. Ist der Backfill geprüft und die Konsistenz bestätigt, schaltet das Flag die Lesevorgänge auf die neue Spalte um. Stimmt etwas nicht, schaltet man zurück — ohne neue Auslieferung, in Sekunden.
Damit wird aus einem riskanten Stichtag ein umkehrbarer, beobachtbarer Schalter. Das ist der eigentliche Gewinn: Die gefährlichste Sekunde einer Migration — die Umstellung des Lesens — wird von der Auslieferung entkoppelt und jederzeit rücknehmbar. Ein Flag erlaubt zudem, die Umstellung nicht als Alles-oder-nichts zu fahren, sondern zunächst für einen kleinen Teil des Verkehrs; man beobachtet, und weitet aus, wenn nichts abweicht. So wird selbst die letzte, gefährlichste Sekunde graduell und beobachtbar statt zu einem Sprung ins Dunkle.
Die Empfehlung: Entkoppeln Sie die Umstellung des Lesens per Feature-Flag von der Auslieferung. Der Preis ist zusätzlicher Zustand: Ein Flag ist Code-Verzweigung, die man testen und — wichtig — nach der Migration wieder entfernen muss; ein vergessenes Flag ist dauerhafte Schuld. Anders entscheiden wir bei einer kleinen, risikoarmen Umstellung, deren Rücknahme über einen schnellen erneuten Deploy billiger ist als die Pflege eines Flags.
Datenmigrationen vs. Schemamigrationen
Zwei Dinge werden oft in einen Topf geworfen, verhalten sich aber völlig unterschiedlich. Eine Schemamigration ändert die Struktur — sie ist meist kurz, kann aber sperren. Eine Datenmigration bewegt oder transformiert Zeilen — sie kann riesig und lang sein und darf niemals in einem einzigen sperrenden Schritt laufen.
| Dimension | Schemamigration | Datenmigration |
|---|---|---|
| Was ändert sich | Struktur (DDL) | Inhalt der Zeilen |
| Typische Dauer | kurz | potenziell sehr lang |
| Sperrrisiko | hoch bei großen Tabellen | niedrig, wenn gebatcht |
| Wann ausgeführt | im Deploy-Schritt | nebenläufig, außerhalb des Deploys |
| Muss idempotent sein | — | ja: wiederholbar und fortsetzbar |
| Rücknahme | teils, über additive Schritte | über Roll-forward, nicht rückwärts |
Der entscheidende Fehler ist, eine große Datenmigration in den Deploy-Schritt zu legen: Sie blockiert die Auslieferung und sperrt womöglich die Tabelle. Richtig ist, sie als kontrollierten Hintergrundjob zu führen — in Stapeln, idempotent und fortsetzbar, gedrosselt, damit sie die Datenbank nicht überlastet, und mit sichtbarem Fortschritt.
Die Stapelgröße ist dabei die zentrale Stellschraube: Kleine Stapel halten jede Transaktion kurz und die Sperren gering, kosten aber mehr Durchläufe; große Stapel sind schneller, riskieren aber lange Transaktionen und Druck auf die Datenbank. Zwischen den Stapeln pausiert der Job kurz, beobachtet die Last und drosselt sich selbst, wenn Antwortzeiten oder Replikationsverzögerung steigen. Ein Kontrollpunkt nach jedem Stapel macht ihn fortsetzbar: Bricht er ab, beginnt er nicht von vorn, sondern beim letzten Punkt — und weil er idempotent ist, schadet auch ein doppelt verarbeiteter Stapel nicht.
Die Empfehlung: Führen Sie Datenmigrationen nebenläufig, gebatcht, idempotent und gedrosselt aus — getrennt vom Deploy. Der Preis ist mehr Maschinerie als ein einzelnes Skript: Fortschritt, Wiederaufnahme und Drosselung wollen gebaut sein. Anders entscheiden wir bei kleinen Datenmengen, die in einem kurzen, unkritischen Schritt umgeschrieben sind; dort ist der einmalige Lauf einfacher als ein fortsetzbarer Job.
Rollback-Strategie
Die wichtigste Einsicht der Zero-Downtime-Migration ist zugleich die beruhigendste: Man rollt den Code zurück, nicht die Datenbank. Weil jeder Schema-Zustand rückwärtskompatibel ist, ist das Zurückrollen der Anwendung immer sicher — die zusätzliche Spalte sitzt einfach ungenutzt da und stört niemanden. Die Datenbank muss dafür nicht angefasst werden.
| Schritt | Zurücknehmbar durch |
|---|---|
| Expand (additiv) | Code zurückrollen; das Schema darf bleiben |
| Backfill | erneut oder korrigierend füllen (idempotent) |
| Umstellung des Lesens | Feature-Flag zurückschalten |
| Contract (Entfernen) | nicht rücknehmbar — nur Roll-forward |
Die einzige Ausnahme ist der Contract-Schritt. Eine entfernte Spalte kommt nicht zurück. Deshalb ist Contract der letzte Schritt, ausgeführt erst, wenn über echten Verkehr Vertrauen entstanden ist und niemand mehr das Alte braucht. Bis dahin behält man die alte Spalte und ihre Daten.
Das hat eine praktische Konsequenz für die Migrationswerkzeuge. Viele erlauben eine „Ab"-Migration, die eine Änderung automatisch rückgängig macht. Für additive Schritte ist sie harmlos; für den Contract-Schritt schreibt man sie besser gar nicht. Eine automatische Rücknahme, die eine gelöschte Spalte samt Daten wiederherstellen soll, kann die Daten nicht zurückbringen — sie erzeugt nur die trügerische Struktur ohne Inhalt und wiegt in falscher Sicherheit. Der ehrliche Weg zurück aus einem Fehler nach dem Contract ist immer der nach vorn: das Fehlende neu aufbauen, nicht die Zeit zurückdrehen.
Die Empfehlung: Legen Sie jede Migration so an, dass ein Rollback durch reines Zurückrollen des Codes möglich ist, und schieben Sie das Zerstörende bis zuletzt auf. Der Preis ist, dass die alte Struktur eine Weile mitläuft und man diszipliniert sein muss, den Contract-Schritt später wirklich auszuführen. Anders entscheiden wir bei einer additiven Änderung ohne zerstörenden Teil; dort gibt es nichts Endgültiges, und die Sorgfalt gilt nur dem Sperrverhalten.
Beobachtbarkeit während der Migration
Eine Zero-Downtime-Migration lebt davon, dass man sieht, was passiert — Hoffen ist keine Strategie. Vier Dinge gehören unter Beobachtung, von der ersten Minute an, nicht erst nach dem ersten Vorfall.
Erstens Sperren: Wartet eine Migration auf eine Sperre oder hält sie eine, stauen sich Anfragen — das muss sofort sichtbar sein. Zweitens die Replikationsverzögerung: Ein schwerer Backfill kann Replikate hinterherhinken lassen, sodass Lesevorgänge veraltete Daten sehen. Drittens Fehlerrate und Antwortzeiten während und nach jedem Schritt, um eine Regression sofort einem Schritt zuzuordnen. Viertens die Konsistenz des Doppelschreibens: Bevor das Lesen umgestellt wird, vergleicht man alt und neu und stellt erst um, wenn sie übereinstimmen. Diesen Vergleich führt man nicht einmalig, sondern laufend während der Migrate-Phase — über Stichproben oder eine Prüfsumme beider Spalten —, bis die Abweichung dauerhaft null ist. Eine verbleibende Abweichung ist fast immer ein vergessener Schreibpfad, der noch nicht doppelt schreibt, und genau den will man finden, bevor das Lesen umgestellt wird.
Die Empfehlung: Überwachen Sie Sperren, Replikationsverzögerung, Fehlerrate und Doppelschreib-Konsistenz über jeden Schritt hinweg. Der Preis ist Instrumentierung und Wachsamkeit, die am reibungslosen Tag nichts sichtbar produziert. Anders entscheiden wir bei einer kleinen, additiven Änderung ohne Backfill; dort genügt die normale Betriebsüberwachung, weil weder Sperre noch Verzögerung noch Doppelschreiben im Spiel sind.
Häufige Fehler
Die immer gleichen Muster verwandeln eine ruhige Migration in einen Ausfall:
- Die brechende Änderung in einem Schritt — Umbenennen, Entfernen, Typänderung ohne Expand/Contract.
- Schema- und Code-Änderung in derselben Auslieferung, sodass die alte Version am neuen Schema zerbricht.
- Eine lang sperrende Operation auf einer großen Tabelle mitten im Deploy.
- Der Backfill in einer einzigen riesigen Transaktion, die sperrt und bei Abbruch von vorn beginnt.
- Eine Datenmigration, die weder idempotent noch fortsetzbar ist.
- Die alte Spalte zu früh entfernt, während die alte Version noch läuft.
- Ein zerstörendes Rollback der Datenbank, das Daten unwiederbringlich verliert.
- Keine Beobachtung von Sperren und Replikationsverzögerung — die Migration läuft blind.
- Der Contract-Schritt, der nie kommt: Flag und Übergangsspalte bleiben für immer, der Zwischenzustand wird zum Dauerzustand.
Entscheidungs-Checkliste
Fragen vor jeder Schemaänderung. Es sind Diagnosefragen, keine Urteile.
- Passt jeder Schema-Zustand zur laufenden und zur nächsten Codeversion? Sonst bricht die rollierende Auslieferung.
- Ist jeder Schritt einzeln auslieferbar und umkehrbar — außer dem finalen Contract? Ein nicht umkehrbarer Zwischenschritt ist ein verstecktes Wartungsfenster.
- Nimmt eine Operation eine lange Sperre auf einer großen Tabelle? Dann nebenläufig ausführen oder zerlegen.
- Ist die Datenmigration gebatcht, idempotent, fortsetzbar, gedrosselt — und außerhalb des Deploys? Große Backfills gehören nicht in den Auslieferungsschritt.
- Können wir allein durch Zurückrollen des Codes zurück, ohne die Datenbank anzufassen? Das ist der Prüfstein für Rückwärtskompatibilität.
- Ist der zerstörende Contract-Schritt aufgeschoben, bis Vertrauen besteht? Entfernen ist endgültig.
- Beobachten wir Sperren, Replikationsverzögerung, Fehlerrate und Doppelschreib-Konsistenz? Blind migrieren heißt hoffen.
- Gibt es einen Verantwortlichen und einen Termin, um Übergangsspalte und Flag zu entfernen? Sonst bleibt der Zwischenzustand.
FAQ
Brauchen wir das für jede Änderung? Nein. Eine additive, nullable Spalte oder eine neue Tabelle ist ein einziger sicherer Schritt. Der volle Expand/Contract-Ablauf gilt nur für brechende Änderungen — Umbenennen, Entfernen, Typänderung. Der Aufwand richtet sich nach der Art der Änderung, nicht nach Gewohnheit.
Ist ein kurzes Wartungsfenster nicht einfacher? Manchmal ja. Für kleine Systeme, kleine Datenmengen und tolerierbaren Stillstand ist die einmalige Änderung im Fenster billiger als der mehrstufige Ablauf. Zero-Downtime lohnt sich, wenn Stillstand teuer ist oder das Datenvolumen eine Operation lang genug macht, dass „kurz" nicht mehr stimmt.
Wie lange behalten wir die alte Spalte? Bis nichts mehr sie liest oder schreibt und über echten Verkehr Vertrauen entstanden ist. Dann — und erst dann — kommt der Contract-Schritt. Die alte Spalte ist Ihre Rückfahrkarte; man wirft sie nicht weg, solange man sie noch brauchen könnte.
Gilt das auch für schemalose Datenbanken? Ja. „Schemalos" heißt nur, dass das Schema implizit im Code steht statt in der Datenbank. Die Form der Daten ändert sich trotzdem, alte und neue Version lesen dieselben Dokumente, und Expand/Contract gilt unverändert — additiv erweitern, umziehen, altes Feld zuletzt entfernen.
Wer führt die Datenmigration aus? Ein kontrollierter Hintergrundjob, nicht der Deploy. Er läuft nebenläufig, in Stapeln, gedrosselt und fortsetzbar, mit sichtbarem Fortschritt. So blockiert er weder die Auslieferung noch die Datenbank und kann jederzeit pausiert und wieder aufgenommen werden.
Was, wenn der Backfill Tage dauert? Das ist in Ordnung. Er läuft online und gedrosselt, ohne das System zu stören; die Umstellung des Lesens wartet einfach, bis er fertig und geprüft ist. Ein langer Backfill ist kein Problem, solange er nebenläufig und fortsetzbar ist — nur ein sperrender wäre eines.
Weiterführend
- Legacy-Modernisierung ohne Big Bang — dieselbe Expand/Contract-Idee auf ganze Systeme angewandt.
- API-Versionierung ohne Kunden zu brechen — additives, rückwärtskompatibles Ändern an der Schnittstelle.
- Wann sich Tests wirklich lohnen — warum die Konsistenzprüfung des Doppelschreibens der wichtigste Test der Migration ist.
- Idempotenz und PostgreSQL: JSONB vs. Tabellen — verwandte Bausteine an der Datenschicht.
Grundlage ist die Batunet Engineering Method: in kleinen, umkehrbaren Schritten bauen, für den Fehlerfall entwerfen, Beobachtbarkeit ab Tag eins.
Eine gelungene Datenbankmigration merkt niemand. Kein Wartungsfenster, kein Bangen um Mitternacht, kein Moment, in dem alles auf dem Spiel steht — nur eine Spalte, die erscheint, sich füllt und irgendwann verschwindet, während das System die ganze Zeit weiterläuft.
Referenzierte Entitäten
Wo dieser Leitfaden im Weg steht.
Setzen Sie Ihren Engineering-Weg fort.
Verwandte Konzepte, Entscheidungen, Playbooks und Standpunkte — als zusammenhängender Pfad, nicht als Linkliste.
Technologien
Verwandte Konzepte
Engineering-Entscheidungen
Ein konkretes Vorhaben in diesem Feld?
Reference Guides zeigen, wie wir denken. Für Ihr System sprechen Sie mit der Geschäftsführung — technisch, ohne Vertrieb.
