Lernen Sie, wie Sie mit Claude Code PostgreSQL‑Migrationen sicher planen: Expand‑Contract‑Pattern, Backfills und Rollback‑Pläne sowie Prüfungen in Staging vor dem Release.

Eine PostgreSQL‑Schemaänderung wirkt einfach, bis sie auf echten Traffic und reale Daten trifft. Das Gefährliche ist meist nicht das SQL selbst, sondern wenn App‑Code, Datenbankzustand und Deployment‑Timing aus dem Takt geraten.
Die meisten Ausfälle sind praktisch und schmerzhaft: Ein Deploy bricht, weil alter Code auf eine neue Spalte zugreift, eine Migration blockiert eine heiße Tabelle und Timeouts schießen hoch, oder eine „schnelle“ Änderung löscht oder überschreibt stillschweigend Daten. Selbst wenn nichts abstürzt, können subtile Fehler eingeschleust werden, z. B. falsche Defaults, kaputte Constraints oder Indizes, die nie fertig gebaut wurden.
KI‑generierte Migrationen fügen eine weitere Risikostufe hinzu. Tools können gültiges SQL erzeugen, das für Ihre Workload, Ihr Datenvolumen oder Ihren Release‑Prozess trotzdem unsicher ist. Sie können Tabellennamen raten, langlaufende Locks übersehen oder beim Rollback ausweichen, weil Down‑Migrations schwer sind. Wenn Sie Claude Code für Migrationen verwenden, brauchen Sie Leitplanken und konkreten Kontext.
Wenn dieser Beitrag sagt, eine Änderung sei „sicher“, bedeutet das drei Dinge:
Das Ziel ist, Migrationen zur Routinearbeit zu machen: vorhersehbar, testbar und langweilig.
Beginnen Sie mit ein paar unverhandelbaren Regeln. Sie halten das Modell fokussiert und verhindern, dass Sie eine Änderung ausliefern, die nur auf Ihrem Laptop funktioniert.
Arbeiten Sie in kleinen Schritten. Eine Schemaänderung, ein Daten‑Backfill, eine App‑Änderung und ein Cleanup‑Schritt sind unterschiedliche Risiken. Sie zusammenzufassen macht es schwerer zu sehen, was kaputt ging, und schwerer zurückzurollen.
Bevorzugen Sie additive Änderungen vor destruktiven. Eine Spalte, ein Index oder eine Tabelle hinzuzufügen ist meist geringes Risiko. Umbenennen oder Löschen von Objekten verursacht Ausfälle. Machen Sie zuerst den sicheren Teil, migrieren Sie die App, und entfernen Sie das Alte erst, wenn Sie sicher sind, dass es nicht mehr genutzt wird.
Machen Sie die App für beide Formen tolerant. Der Code sollte während des Rollouts entweder die alte Spalte oder die neue lesen können. Das vermeidet das klassische Rennen, bei dem einige Server neue Version und die Datenbank noch alt sind (oder umgekehrt).
Behandeln Sie Migrationen wie Produktionscode, nicht als schnelles Skript. Selbst wenn Sie mit einer Plattform wie Koder.ai bauen (Go‑Backend mit PostgreSQL, plus React‑ oder Flutter‑Clients), wird die Datenbank von allem gemeinsam genutzt. Fehler sind teuer.
Wenn Sie eine kompakte Regelmenge an den Anfang jeder SQL‑Anfrage setzen wollen, verwenden Sie etwas wie:
Ein praktisches Beispiel: Statt eine abhängige Spalte umzubenennen, fügen Sie die neue Spalte hinzu, füllen sie langsam nach, deployen Code, der zuerst neu liest und dann auf alt zurückfällt, und entfernen die alte Spalte erst später.
Claude kann ordentliches SQL aus einer vagen Anforderung schreiben, aber sichere Migrationen brauchen Kontext. Behandeln Sie Ihren Prompt wie ein Mini‑Design‑Brief: zeigen Sie, was existiert, erklären Sie, was nicht kaputtgehen darf, und definieren Sie, was „sicher“ für Ihren Rollout bedeutet.
Beginnen Sie, indem Sie nur die Datenbank‑Fakten einfügen, die relevant sind. Schließen Sie Tabellen‑Definitionen sowie relevante Indizes und Constraints ein (Primary Keys, Unique‑Constraints, Foreign Keys, Check‑Constraints, Trigger). Wenn verwandte Tabellen beteiligt sind, fügen Sie diese Snippets ebenfalls bei. Ein kleines, genaues Exzerpt verhindert, dass das Modell Namen rät oder eine wichtige Constraint übersieht.
Geben Sie reale Skalierung an. Zeilenanzahlen, Tabellengröße, Schreibrate und Spitzenverkehr sollten den Plan beeinflussen. „200M Zeilen und 1k Writes/s“ ist eine andere Migration als „20k Zeilen und überwiegend Lesezugriffe“. Geben Sie außerdem Ihre Postgres‑Version an und wie Migrationen in Ihrem System laufen (eine einzige Transaktion vs. mehrere Schritte).
Beschreiben Sie, wie die Anwendung die Daten nutzt: die wichtigen Reads, Writes und Background‑Jobs. Beispiele: „API liest nach E‑Mail“, „Worker updaten Status“ oder „Berichte scannen nach created_at“. Das entscheidet, ob Sie expand/contract, Feature Flags und wie sicher ein Backfill ist, brauchen.
Seien Sie zuletzt explizit bei Constraints und Deliverables. Eine einfache Struktur funktioniert gut:
Wenn Sie sowohl SQL als auch einen Runplan verlangen, zwingt das Modell, über Reihenfolge, Risiko und Prüfungen nachzudenken.
Das Expand/Contract‑Migrationsmuster ändert eine PostgreSQL‑Datenbank, ohne die App zu unterbrechen, während die Änderung in Arbeit ist. Statt eines einzelnen riskanten Schalters unterstützt die Datenbank vorübergehend beide Formen (alt und neu).
Denken Sie daran: sicher Neues hinzufügen (expand), Traffic und Daten schrittweise verschieben, und erst danach das Alte entfernen (contract). Das ist besonders hilfreich bei KI‑unterstützter Arbeit, weil es Sie zwingt, die unordentliche Mitte zu planen.
Ein praktischer Ablauf sieht so aus:
Verwenden Sie dieses Muster immer dann, wenn Benutzer noch mit alter App‑Version unterwegs sein können. Dazu gehören Multi‑Instance‑Deployments, mobile Apps, die langsam updaten, oder Releases, deren Migration Minuten oder Stunden dauern kann.
Eine hilfreiche Taktik ist, zwei Releases zu planen. Release 1 macht Expand plus Kompatibilität, sodass nichts bricht, wenn der Backfill unvollständig ist. Release 2 macht den Contract erst, nachdem Sie bestätigen, dass neuer Code und neue Daten vorhanden sind.
Kopieren Sie diese Vorlage und füllen Sie die eckigen Klammern. Sie zwingt Claude Code dazu, ausführbares SQL, Prüfungen und einen realistischen Rollback‑Plan zu liefern.
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
Zwei zusätzliche Zeilen, die in der Praxis helfen:
RISK: blocks writes zu kennzeichnen, plus wann er ausgeführt werden sollte (Off‑Peak vs. jederzeit).Kleine Schemaänderungen können trotzdem Schaden anrichten, wenn sie lange Locks verursachen, große Tabellen neu schreiben oder halb schief gehen. Wenn Sie Claude Code für Migrationen nutzen, fordern Sie SQL an, das Rewrites vermeidet und Ihre App funktionsfähig hält, während die Datenbank nachzieht.
Eine nullable Spalte hinzuzufügen ist normalerweise sicher. Eine Spalte mit non‑null Default kann auf älteren Postgres‑Versionen riskant sein, weil sie die ganze Tabelle umschreiben kann.
Ein sichererer Ansatz ist eine Zwei‑Schritt‑Änderung: Spalte zuerst NULL und ohne Default hinzufügen, in Chargen backfillen, dann Default für neue Zeilen setzen und NOT NULL erst einführen, wenn die Daten sauber sind.
Wenn Sie sofort einen Default brauchen, verlangen Sie eine Erklärung des Lock‑Verhaltens für Ihre Postgres‑Version und einen Fallback‑Plan für den Fall, dass die Laufzeit länger ist als erwartet.
Für Indizes auf großen Tabellen fordern Sie CREATE INDEX CONCURRENTLY, damit Reads und Writes weiterlaufen. Bitten Sie außerdem um den Hinweis, dass dieser Befehl nicht in einem Transaktionsblock laufen kann, also Ihr Migrationstool einen nicht‑transaktionalen Schritt unterstützen muss.
Bei Foreign Keys ist der sicherere Weg meist, die Constraint zuerst als NOT VALID hinzuzufügen und später zu validieren. Das macht den ersten Schritt schneller, während neue Writes weiterhin abgesichert werden.
Wenn Sie Constraints strenger machen (NOT NULL, UNIQUE, CHECK), fordern Sie: „erst bereinigen, dann erzwingen.“ Die Migration sollte fehlerhafte Zeilen finden, reparieren und erst dann die strengere Regel aktivieren.
Wenn Sie eine kurze Checkliste in Prompts einfügen möchten, halten Sie sie knapp:
Backfills sind der Ort, an dem die meisten Migrationsschmerzen auftreten, nicht das ALTER TABLE. Sichere Prompts behandeln Backfills wie kontrollierte Jobs: messbar, wiederaufnehmbar und schonend für Produktion.
Beginnen Sie mit Akzeptanzchecks, die leicht auszuführen und schwer zu diskutieren sind: erwartete Zeilenzahlen, Ziel‑Null‑Rate und ein paar Stichprobenprüfungen (z. B. alte vs. neue Werte für 20 zufällige IDs).
Dann fordern Sie einen Batch‑Plan. Batches halten Locks kurz und reduzieren Überraschungen. Eine gute Anfrage spezifiziert:
Fordern Sie Idempotenz, weil Backfills mitten drin fehlschlagen. Das SQL sollte erneut ausführbar sein, ohne Duplikate oder Korruption zu erzeugen. Typische Muster sind „update only where the new column is NULL“ oder deterministische Regeln, bei denen die gleiche Eingabe immer das gleiche Ergebnis erzeugt.
Geben Sie auch an, wie die App während des Backfills korrekt bleibt. Wenn neue Writes ankommen, brauchen Sie eine Brücke: Dual‑Write im App‑Code, ein temporärer Trigger oder Read‑Fallback‑Logik (neu lesen, sonst alt). Sagen Sie, welche Methode Sie sicher einsetzen können.
Schließlich bauen Sie Pause und Resume in das Design ein. Fordern Sie Fortschritts‑Tracking und Checkpoints, z. B. eine kleine Tabelle, die die zuletzt verarbeitete ID speichert, und eine Abfrage, die den Fortschritt berichtet (aktualisierte Zeilen, letzte ID, Startzeit).
Beispiel: Sie fügen users.full_name hinzu, abgeleitet aus first_name und last_name. Ein sicherer Backfill aktualisiert nur Zeilen, bei denen full_name IS NULL ist, läuft in ID‑Bereichen, protokolliert die letzte aktualisierte ID und hält neue Registrierungen über Dual‑Write korrekt, bis die Umschaltung abgeschlossen ist.
Ein Rollback‑Plan ist nicht nur „schreibe ein Down‑Migration“. Es sind zwei Probleme: das Schema rückgängig machen und mit Daten umgehen, die während des Betriebs verändert wurden. Schema‑Rollback ist oft möglich. Daten‑Rollback ist es meist nicht, es sei denn, Sie haben im Voraus dafür geplant.
Seien Sie explizit, was Rollback für Ihre Änderung bedeutet. Wenn Sie eine Spalte droppen oder Werte in‑place umschreiben, verlangen Sie eine realistische Antwort wie: „Rollback stellt App‑Kompatibilität wieder her, aber originale Daten können ohne Snapshot nicht wiederhergestellt werden.“ Diese Ehrlichkeit schützt Sie.
Fordern Sie klare Rollback‑Trigger, damit es im Incident nicht zu Diskussionen kommt. Beispiele:
Verlangen Sie das komplette Rollback‑Paket, nicht nur SQL: Down‑Migrations‑SQL (nur wenn sicher), App‑Konfig/Code‑Schalter für Kompatibilität und wie man Background‑Jobs stoppt.
Dieses Prompt‑Muster reicht normalerweise aus:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
Bevor Sie ausliefern, erfassen Sie einen leichten „Safety Snapshot“, damit Sie Vorher/Nachher vergleichen können:
Seien Sie ebenfalls klar darüber, wann nicht zu rollen ist. Wenn Sie nur eine nullable Spalte hinzugefügt haben und die App dual‑write macht, ist eine Vorwärts‑Korrektur (Hotfix‑Code, Backfill pausieren, dann fortsetzen) oft sicherer, als zurückzurollen und noch mehr Drift zu erzeugen.
KI kann schnell SQL schreiben, aber sie sieht Ihre Produktionsdatenbank nicht. Die meisten Fehler passieren, wenn der Prompt vage ist und das Modell Lücken füllt.
Eine typische Falle ist, das aktuelle Schema wegzulassen. Wenn Sie keine Tabellen‑Definition, Indizes und Constraints einfügen, kann das SQL Spalten adressieren, die nicht existieren, oder eine Unique‑Regel übersehen, die einen Backfill in einen langsamen, lock‑schweren Vorgang verwandelt.
Ein weiterer Fehler ist, Expand, Backfill und Contract in einem Deploy zu shippen. Das nimmt Ihnen die Fluchtmöglichkeit. Wenn der Backfill lange läuft oder mitten drin fehlschlägt, haben Sie eine App, die bereits den Endzustand erwartet.
Die Probleme, die am häufigsten auftreten:
Ein konkretes Beispiel: „Spalte umbenennen und App updaten.“ Wenn der generierte Plan Umbenennung und Backfill in einer Transaktion macht, kann ein langsamer Backfill Locks halten und den Live‑Traffic kaputtmachen. Ein sicherer Prompt erzwingt kleine Chargen, explizite Timeouts und Verifizierungsabfragen, bevor der alte Pfad entfernt wird.
Staging ist der Ort, an dem Probleme auftauchen, die in einer kleinen Dev‑DB nie sichtbar werden: lange Locks, überraschende Nulls, fehlende Indizes und vergessene Codepfade.
Prüfen Sie zuerst, dass das Schema nach der Migration dem Plan entspricht: Spalten, Typen, Defaults, Constraints und Indizes. Ein flüchtiger Blick reicht nicht. Ein fehlender Index kann einen sicheren Backfill in ein langsames Desaster verwandeln.
Führen Sie die Migration gegen einen realistischen Datensatz durch. Idealerweise ist das eine aktuelle Kopie der Produktion mit Maskierung sensibler Felder. Wenn das nicht möglich ist, stimmen Sie zumindest Volumen und Hotspots ab (große Tabellen, breite Rows, viele Indizes). Protokollieren Sie die Laufzeiten für jeden Schritt, damit Sie wissen, was in Produktion zu erwarten ist.
Eine kurze Staging‑Checkliste:
Testen Sie schließlich echte Nutzerflüsse, nicht nur SQL. Erstellen, aktualisieren und lesen Sie Datensätze, die von der Änderung betroffen sind. Wenn Expand/Contract geplant ist, stellen Sie sicher, dass beide Schemata bis zum finalen Cleanup funktionieren.
Stellen Sie sich vor, Sie haben eine users.name‑Spalte mit vollen Namen wie „Ada Lovelace“. Sie wollen first_name und last_name, dürfen aber Signups, Profile oder Admin‑Seiten während des Rollouts nicht unterbrechen.
Beginnen Sie mit einem Expand‑Schritt, der sicher ist, auch wenn kein Code‑Change deployed wird: nullable Spalten hinzufügen, die alte Spalte behalten und lange Locks vermeiden.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Dann ändern Sie das App‑Verhalten, sodass Release 1 beide Schemata unterstützt: Die App liest neue Spalten, wenn vorhanden, fällt sonst auf name zurück, und schreibt vorzugsweise in beide, damit neue Daten konsistent bleiben.
Als Nächstes kommt der Backfill. Führen Sie einen Batch‑Job aus, der kleine Mengen von Zeilen pro Lauf aktualisiert, den Fortschritt protokolliert und sicher pausierbar ist. Zum Beispiel: update users where first_name is null` in aufsteigender ID‑Reihenfolge, 1.000 auf einmal, und loggen Sie die Anzahl der geänderten Zeilen.
Bevor Sie Regeln verschärfen, validieren Sie in Staging:
first_name und last_name und setzen weiterhin namename vorhanden istusers sind nicht merklich langsamerRelease 2 stellt die Reads ausschließlich auf die neuen Spalten um. Erst danach sollten Sie Constraints hinzufügen (z. B. SET NOT NULL) und name löschen, idealerweise in einem späteren, separaten Deploy.
Für den Rollback: Halten Sie es langweilig. Die App liest während der Transition weiter name, und der Backfill ist stoppbar. Wenn Sie Release 2 zurückrollen müssen, schalten Sie die Reads zurück auf name und lassen die neuen Spalten stehen, bis wieder Stabilität herrscht.
Behandeln Sie jede Änderung wie ein kleines Runbook. Das Ziel ist nicht ein perfekter Prompt, sondern eine Routine, die die richtigen Details erzwingt: Schema, Constraints, Run‑Plan und Rollback.
Standardisieren Sie, was jede Migrationsanfrage enthalten muss:
Bestimmen Sie vorab Verantwortlichkeiten, bevor jemand SQL ausführt. Eine einfache Aufteilung verhindert „jeder dachte, jemand anderes macht es“: Entwickler verantworten Prompt und Migrationscode, Ops die Produktionszeitplanung und das Monitoring, QA verifiziert Staging‑Verhalten und Edge‑Cases, und eine Person hat das finale Go/No‑Go.
Wenn Sie Apps per Chat bauen, hilft es, die Sequenz aufzuschreiben, bevor Sie SQL generieren. Für Teams, die Koder.ai nutzen, ist Planning Mode ein natürlicher Ort, diese Sequenz zu dokumentieren; Snapshots plus Rollback reduzieren die Blast‑Radius, wenn während des Rollouts etwas Unerwartetes passiert.
Nachdem Sie ausgeliefert haben, planen Sie das Contract‑Cleanup zeitnah, solange der Kontext frisch ist, damit alte Spalten und temporärer Kompatibilitätscode nicht monatelang herumliegen.
Eine Schemaänderung ist riskant, wenn App‑Code, Datenbankzustand und Deployment‑Timing nicht mehr übereinstimmen.
Häufige Fehlerbilder:
Verwenden Sie das Expand/Contract‑Vorgehen:
Das Modell kann gültiges SQL erzeugen, das aber für Ihre Arbeitslast unsicher ist.
Typische AI‑spezifische Risiken:
Behandeln Sie AI‑Output als Entwurf und verlangen Sie Runplan, Prüfungen und Rollback‑Schritte.
Fügen Sie nur die Fakten ein, von denen die Migration abhängt:
CREATE TABLE‑Snippets (plus Indizes, FKs, UNIQUE/CHECK‑Constraints, Trigger)Standardregel: trennen.
Ein praktischer Ablauf:
Alles zusammenzulegen macht Fehler schwieriger zu diagnostizieren und zurückzunehmen.
Bevorzugen Sie dieses Muster:
ADD COLUMN ... NULL ohne Default (schnell)NOT NULL erst nach Verifikation hinzufügenEin sofortiger non‑null Default kann auf manchen Postgres‑Versionen ein Full‑Table‑Rewrite erzwingen. Wenn ein sofortiger Default nötig ist, verlangen Sie eine Erklärung des Lock‑Verhaltens und einen Fallback‑Plan.
Fordern Sie:
CREATE INDEX CONCURRENTLY für große/heiß genutzte TabellenZur Verifikation: prüfen Sie schnell, ob der Index existiert und genutzt wird (z. B. EXPLAIN‑Plan vor/nach in Staging).
Fügen Sie das FK zuerst als NOT VALID hinzu und validieren Sie später:
NOT VALID hinzufügen, damit der erste Schritt weniger disruptiv istVALIDATE CONSTRAINT separat ausführen, wenn Sie den Vorgang überwachen könnenSo wird das FK für neue Writes durchgesetzt, während Sie kontrollieren, wann die teure Validierung läuft.
Ein guter Backfill ist gebatcht, idempotent und wiederaufnehmbar.
Praktische Anforderungen:
WHERE new_col IS NULL)Ziel des Rollbacks: schnell die App‑Kompatibilität wiederherstellen, auch wenn die Daten nicht perfekt zurückgedreht werden können.
Ein brauchbarer Rollback‑Plan sollte enthalten:
Oft ist der sicherste Rollback, die Reads zurück auf das alte Feld zu schalten und die neuen Spalten stehen zu lassen.
So bleiben alte und neue App‑Versionen während des Rollouts funktionsfähig.
Das verhindert Raten und erzwingt die richtige Reihenfolge.
Das macht Backfills überlebbar unter echter Last.