Naucz się, jak używać Claude Code do migracji PostgreSQL bezpiecznie: expand-contract, backfille, plan rollbacku i co sprawdzić w stagingu przed wydaniem.

Zmiana schematu PostgreSQL wydaje się prosta, dopóki nie spotka się z prawdziwym ruchem i prawdziwymi danymi. Ryzyko zwykle nie tkwi w samym SQL. Pojawia się wtedy, gdy kod aplikacji, stan bazy i timing wdrożenia przestają być spójne.
Większość awarii jest praktyczna i bolesna: wdrożenie łamie się, bo stary kod odwołuje się do nowej kolumny, migracja blokuje gorącą tabelę i pojawiają się timeouty, albo „szybka” zmiana cicho usuwa lub przepisuje dane. Nawet gdy nic nie pada, możesz wypuścić subtelne błędy: złe wartości domyślne, uszkodzone ograniczenia czy indeksy, które nigdy się nie zbudowały.
Migracje generowane przez AI dokładają kolejną warstwę ryzyka. Narzędzia mogą wygenerować poprawny SQL, który i tak jest niebezpieczny dla twojego obciążenia, wolumenu danych czy procesu wydania. Mogą też zgadywać nazwy tabel, pominąć długotrwałe blokady albo zbagatelizować rollback, bo down migracje są trudne. Jeśli używasz Claude Code do migracji, potrzebujesz zabezpieczeń i konkretnego kontekstu.
Gdy w tym poście mówimy, że zmiana jest "bezpieczna", oznacza to trzy rzeczy:
Celem jest, aby migracje stały się rutynową pracą: przewidywalną, testowalną i nudną.
Zacznij od kilku niepodważalnych reguł. Pomagają skupić model i chronią przed wysłaniem zmiany, która działa tylko na twoim laptopie.
Rozdziel pracę na małe kroki. Zmiana schematu, backfill danych, zmiana w aplikacji i krok cleanup to różne ryzyka. Ich zgrupowanie utrudnia ustalenie, co się zepsuło i utrudnia rollback.
Preferuj operacje addytywne przed destrukcyjnymi. Dodanie kolumny, indeksu czy tabeli zwykle jest niskiego ryzyka. Zmiany nazwy lub usunięcia to miejsca, gdzie zdarzają się outage'y. Zrób najpierw bezpieczną część, przenieś aplikację, a stare elementy usuń dopiero gdy będziesz pewien, że są nieużywane.
Spraw, by aplikacja tolerowała obie struktury przez pewien czas. Kod powinien móc czytać albo starą kolumnę, albo nową w czasie rolloutu. To zapobiega popularnemu wyścigowi, gdy niektóre serwery uruchomione są z nowym kodem, a baza jest jeszcze stara (lub odwrotnie).
Traktuj migracje jak kod produkcyjny, a nie szybki skrypt. Nawet jeśli budujesz na platformie takiej jak Koder.ai (backend w Go z PostgreSQL, plus klienci w React lub Flutter), baza jest współdzielona przez wszystko. Błędy kosztują dużo.
Jeśli chcesz krótkiego zestawu zasad, które możesz wkleić na górę każdego żądania SQL, użyj czegoś w stylu:
Praktyczny przykład: zamiast zmienić nazwę kolumny, od której zależy aplikacja, dodaj nową kolumnę, backfill ją powoli, wdroż kod, który czyta najpierw nową, potem starą, i dopiero później usuń starą kolumnę.
Claude potrafi napisać sensowny SQL na podstawie ogólnego polecenia, ale bezpieczne migracje potrzebują kontekstu. Traktuj prompt jak mini brief projektowy: pokaż, co istnieje, wyjaśnij, co nie może się zepsuć, i zdefiniuj, co znaczy „bezpieczne” dla twojego rolloutu.
Zacznij od wklejenia tylko faktów o bazie, które mają znaczenie. Dołącz definicję tabeli oraz istotne indeksy i ograniczenia (klucze główne, UNIQUE, FK, CHECK, triggery). Jeśli zaangażowane są powiązane tabele, dołącz ich fragmenty też. Mały, dokładny wycinek zapobiega zgadywaniu nazw lub pominięciu istotnego ograniczenia.
Dodaj skalę rzeczywistą. Liczba wierszy, rozmiar tabeli, tempo zapisów i ruch szczytowy powinny wpływać na plan. "200M wierszy i 1k zapisów/s" to inna migracja niż "20k wierszy i głównie odczyty". Dołącz też wersję Postgresa i sposób uruchamiania migracji w twoim systemie (jedna transakcja vs wieloetapowo).
Opisz, jak aplikacja używa danych: ważne odczyty, zapisy i joby backgroundowe. Przykłady: "API czyta po emailu", "workerzy aktualizują status" albo "raporty skanują po created_at". To określa, czy potrzebujesz expand/contract, feature flagów i jak bezpieczny będzie backfill.
Na koniec bądź eksplicytny co do ograniczeń i rezultatów. Prosta struktura działa dobrze:
Poproszenie o SQL i plan uruchomienia zmusza model do myślenia o kolejności, ryzyku i co sprawdzić przed releasem.
Wzorzec expand/contract zmiany w PostgreSQL pozwala zmieniać bazę bez łamania aplikacji w czasie pracy. Zamiast ryzykownego jednego przełącznika, baza obsługuje zarówno stary, jak i nowy kształt przez pewien okres.
Pomyśl o tym jak: dodaj nowe rzeczy bezpiecznie (expand), przenieś ruch i dane stopniowo, a dopiero potem usuń stare elementy (contract). To szczególnie przydatne przy pracy wspomaganej AI, bo zmusza do zaplanowania „brudnego środka”.
Praktyczny przebieg wygląda tak:
Używaj tego wzorca zawsze, gdy użytkownicy mogą być na starym kodzie podczas zmiany bazy. To obejmuje deploymenty wieloinstancyjne, aplikacje mobilne aktualizujące się wolno lub jakikolwiek release, gdzie migracja może trwać minuty lub godziny.
Przydatną taktyką jest zaplanowanie dwóch wydań. Wydanie 1 wykonuje expand plus kompatybilność, tak by nic nie psuło się, jeśli backfill jest niekompletny. Wydanie 2 robi contract dopiero po potwierdzeniu, że nowy kod i nowe dane są na miejscu.
Skopiuj ten szablon i wypełnij nawiasy. Wymusza on na Claude Code wygenerowanie wykonalnego SQL, kontroli potwierdzających wykonanie oraz realistycznego planu rollbacku.
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
Dwie dodatkowe linie, które pomagają w praktyce:
RISK: blocks writes, plus kiedy go uruchomić (off-peak vs anytime).(Te dwie linie w promptcie pomagają wyciągnąć od modelu informacje o ryzyku.)
Małe zmiany schematu wciąż mogą zaszkodzić, jeśli powodują długie blokady, przepisywanie dużych tabel lub awarie w połowie. Gdy korzystasz z Claude Code do migracji, proś o SQL, który unika rewritów i utrzymuje aplikację działającą, dopóki baza się nie dogoni.
Dodanie nullable kolumny jest zwykle bezpieczne. Dodanie kolumny z NOT NULL domyślną wartością może być ryzykowne na starszych wersjach Postgresa, bo może przepisać całą tabelę.
Bezpieczniejsze podejście to zmiana w dwóch krokach: dodaj kolumnę jako NULL bez domyślnej wartości, backfill w batchach, potem ustaw domyślną wartość dla nowych wierszy i dodaj NOT NULL po oczyszczeniu danych.
Jeśli musisz wymusić domyślną wartość natychmiast, wymagaj wyjaśnienia zachowania blokad dla twojej wersji Postgresa i planu awaryjnego, gdy wykonanie będzie dłuższe niż oczekiwano.
Dla indeksów na dużych tabelach poproś o CREATE INDEX CONCURRENTLY, żeby odczyty i zapisy mogły dalej działać. Wymagaj również notatki, że nie można go uruchomić wewnątrz transakcji, co oznacza, że twoje narzędzie migracyjne musi obsłużyć krok poza transakcją.
Dla kluczy obcych bezpieczniejsza ścieżka to zwykle dodanie ograniczenia jako NOT VALID, a późniejsze zwalidowanie. To przyspiesza początkowy krok, a nadal egzekwuje FK dla nowych zapisów.
Gdy zaostrzysz reguły (NOT NULL, UNIQUE, CHECK), poproś o „najpierw oczyść, potem egzekwuj”. Migracja powinna znaleźć złe wiersze, poprawić je, a dopiero potem włączyć ostrzejsze reguły.
Jeśli chcesz krótką checklistę do wklejenia do prompta, trzymaj ją zwartą:
Backfille to miejsce, gdzie pojawia się większość bólu migracji, nie ALTER TABLE. Najbezpieczniejsze promptsy traktują backfill jak kontrolowane zadanie: mierzalne, możliwe do wznowienia i łagodne dla produkcji.
Zacznij od checks akceptacyjnych, które są łatwe do uruchomienia i trudne do zakwestionowania: spodziewane liczby wierszy, docelowy udział NULL i kilka losowych sprawdzeń (np. porównaj stare vs nowe wartości dla 20 losowych ID).
Następnie poproś o plan partycjonowania. Partie skracają blokady i zmniejszają niespodzianki. Dobre żądanie określa:
Wymagaj idempotentności, bo backfille padają w połowie. SQL powinien być bezpieczny do ponownego uruchomienia bez duplikowania lub psucia danych. Typowe wzorce to "update tylko tam, gdzie new column IS NULL" lub reguła deterministyczna, gdzie ten sam input zawsze daje ten sam output.
Również napisz, jak aplikacja pozostaje poprawna podczas backfilla. Jeśli nowe zapisy nadal przychodzą, potrzebujesz mostu: dual-write w kodzie aplikacji, tymczasowy trigger lub logika read-fallback (czytaj nową gdy jest, w przeciwnym razie starą). Powiedz, które podejście możesz bezpiecznie wdrożyć.
Na koniec zaplanuj pauzę i wznowienie. Poproś o śledzenie postępu i checkpointy, np. mała tabela przechowująca ostatnie przetworzone ID i zapytanie raportujące postęp (zaktualizowane wiersze, ostatnie ID, czas rozpoczęcia).
Przykład: dodajesz users.full_name pochodzące z first_name i last_name. Bezpieczny backfill aktualizuje tylko wiersze, gdzie full_name IS NULL, działa po zakresach ID, zapisuje ostatnie zaktualizowane ID i utrzymuje poprawność nowych rejestracji przez dual-write dopóki nie skończysz.
Plan rollbacku to nie tylko "napisz down migrację." To dwa problemy: cofnięcie zmian schematu i obsługa danych, które zmieniły się gdy nowa wersja była live. Cofnięcie schematu jest często możliwe. Cofnięcie danych często nie jest, chyba że przygotowałeś snapshot.
Bądź eksplicytny co znaczy rollback dla twojej zmiany. Jeśli usuwasz kolumnę lub przepisywać wartości in-place, wymagaj realistycznej odpowiedzi typu: "Rollback przywraca kompatybilność aplikacji, ale oryginalne dane nie da się odzyskać bez snapshotu." Ta szczerość chroni.
Poproś o jasne wyzwalacze rollbacku, żeby nie było dyskusji podczas incydentu. Przykłady:
Wymagaj całego pakietu rollbacku, nie tylko SQL: DOWN SQL (tylko jeśli naprawdę bezpieczny), kroki w aplikacji potrzebne do kompatybilności i jak zatrzymać joby backgroundowe.
Ten wzorzec prompta zwykle wystarcza:
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.
Zanim wypuścisz, wykonaj lekką "safety snapshot" by porównać przed i po:
Bądź też jasny, kiedy rollbacku nie robić. Jeśli dodałeś nullable kolumnę i aplikacja dual-write, często bezpieczniej jest naprawić do przodu (hotfix, pauza backfilla, wznowienie) niż cofać i tworzyć większe rozbieżności.
AI może szybko napisać SQL, ale nie widzi twojej produkcyjnej bazy. Większość awarii następuje, gdy prompt jest nieprecyzyjny i model wypełnia luki.
Typową pułapką jest pominięcie obecnego schematu. Jeśli nie wkleisz definicji tabeli, indeksów i ograniczeń, SQL może celować w nieistniejące kolumny lub pominąć regułę unikalności, co zamienia backfill w długotrwałą, blokującą operację.
Inny błąd to zrobienie expand, backfill i contract w jednym wdrożeniu. To odbiera punkt wyjścia. Jeśli backfill zajmie długo lub wywali się w połowie, zostaniesz z aplikacją oczekującą finalnego stanu.
Najczęstsze problemy:
Konkretny przykład: "zmień nazwę kolumny i zaktualizuj aplikację." Jeśli wygenerowany plan zmienia nazwę i robi backfill w jednej transakcji, długi backfill może trzymać blokady i złamać ruch. Bezpieczniejszy prompt wymusza małe partie, jawne timeouty i zapytania weryfikacyjne przed usunięciem starej ścieżki.
Staging to miejsce, gdzie znajdziesz problemy, które nigdy nie wychodzą na małej devowej bazie: długie blokady, niespodziewane NULLe, brakujące indeksy i zapomniane ścieżki kodu.
Najpierw sprawdź, że schemat po migracji odpowiada planowi: kolumny, typy, domyślne wartości, ograniczenia i indeksy. Szybkie oko to za mało. Jednen brakujący indeks może zamienić bezpieczny backfill w wolną katastrofę.
Następnie uruchom migrację na realistycznym zbiorze danych. Najlepiej kopia produkcji z zamaskowanymi wrażliwymi polami. Jeśli nie możesz, przynajmniej odwzoruj wolumen i hot spoty produkcyjne (duże tabele, szerokie wiersze, mocno indeksowane). Zapisz czasy dla każdego kroku, żeby wiedzieć, czego spodziewać się w produkcji.
Krótka checklista stagingu:
Na koniec przetestuj rzeczywiste ścieżki użytkownika, nie tylko SQL. Twórz, aktualizuj i czytaj rekordy dotknięte zmianą. Jeśli plan to expand/contract, potwierdź, że oba schematy działają dopóki nie wykonasz finalnego cleanupu.
Wyobraź sobie, że masz kolumnę users.name przechowującą pełne imiona jak "Ada Lovelace." Chcesz first_name i last_name, ale nie możesz zniszczyć rejestracji, profili ani panelu admina podczas rolloutu.
Zacznij od kroku expand, który jest bezpieczny nawet jeśli nie wypuścisz od razu kodu: dodaj nullable kolumny, zostaw starą kolumnę i unikaj długich blokad.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Następnie zaktualizuj zachowanie aplikacji, żeby wspierała oba schematy. W Wydaniu 1 aplikacja powinna czytać z nowych kolumn jeśli są wypełnione, w przeciwnym razie fallbackować do name, i zapisywać do obu, aby nowe dane były spójne.
Potem backfill. Uruchom job, który aktualizuje małe porcje wierszy na raz, zapisuje postęp i można go bezpiecznie wstrzymać. Na przykład: aktualizuj użytkowników gdzie first_name jest null w kolejności rosnących ID, po 1,000 na raz i loguj ile wierszy zmieniono.
Zanim zaostrzysz reguły, zweryfikuj w stagingu:
first_name i last_name i nadal ustawiają namenameusers nie są zauważalnie wolniejszeWydanie 2 przełącza odczyty na nowe kolumny. Dopiero potem dodawaj ograniczenia (np. SET NOT NULL) i usuwaj name, najlepiej w kolejnym, oddzielnym wdrożeniu.
Dla rollbacku trzymaj to prosto. Aplikacja dalej czyta name podczas przejścia, a backfill można zatrzymać. Jeśli musisz cofnąć Wydanie 2, przełącz odczyty z powrotem na name i pozostaw nowe kolumny dopóki wszystko nie będzie stabilne.
Traktuj każdą zmianę jak mały runbook. Celem nie jest perfekcyjny prompt, lecz rutyna, która wymusza właściwe szczegóły: schemat, ograniczenia, plan uruchomienia i rollback.
Ustandaryzuj, co musi zawierać każde żądanie migracji:
Zdecyduj, kto odpowiada za każdy krok zanim ktoś uruchomi SQL. Prosty podział zapobiega "wszyscy myśleli, że ktoś inny to zrobi": deweloperzy odpowiadają za prompt i kod migracji, ops za timing produkcji i monitoring, QA weryfikuje zachowanie w stagingu i edge-cases, a jedna osoba podejmuje ostateczne go/no-go.
Jeśli budujesz aplikacje przez czat, pomocne jest opisanie sekwencji zanim wygenerujesz SQL. Dla zespołów korzystających z Koder.ai, Planning Mode to naturalne miejsce, żeby zapisać tę sekwencję, a snapshoty i rollback zmniejszają powierzchnię rażenia, jeśli coś niespodziewanego zdarzy się podczas rolloutu.
Po wypuszczeniu zaplanuj cleanup contract natychmiast, póki kontekst jest świeży, żeby stare kolumny i tymczasowy kod kompatybilności nie wisiały miesiącami.
Zmiana schematu jest ryzykowna, gdy kod aplikacji, stan bazy i timing wdrożenia przestają być zgodne.
Typowe tryby awarii:
Użyj podejścia expand/contract:
To pozwala obu wersjom aplikacji działać podczas rolloutu.
Model może wygenerować poprawny SQL, który jednak jest niebezpieczny dla twojego obciążenia.
Typowe ryzyka specyficzne dla AI:
Traktuj wynik AI jako szkic i wymagaj planu uruchomienia, zapytań weryfikacyjnych i kroków rollbacku.
Wklej tylko fakty, od których zależy migracja:
CREATE TABLE (plus indeksy, FK, UNIQUE/CHECK, triggery)Domyślna zasada: separuj je.
Praktyczny podział:
Łączenie wszystkiego utrudnia diagnozę i rollback.
Preferuj ten wzorzec:
ADD COLUMN ... NULL bez domyślnej wartości (szybkie)NOT NULL dopiero po weryfikacjiDodanie nie-null default może być ryzykowne w niektórych wersjach, bo może przepisać całą tabelę. Jeśli potrzebujesz natychmiastowego defaultu, wymagaj wyjaśnienia zachowania blokad dla twojej wersji Postgresa i planu awaryjnego.
Poproś o:
CREATE INDEX CONCURRENTLY dla dużych/gorących tabelDla weryfikacji dołącz szybkie sprawdzenie, że indeks istnieje i jest używany (np. porównanie EXPLAIN przed/po w stagingu).
Użyj NOT VALID najpierw, potem VALIDATE:
NOT VALID, żeby początkowy krok był mniej inwazyjnyVALIDATE CONSTRAINT w osobnym kroku, gdy możesz go obserwowaćTo nadal wymusza FK dla nowych zapisów, a jednocześnie pozwala kontrolować kosztowną walidację.
Dobry backfill jest partycjonowany, idempotentny i możliwy do wznowienia.
Praktyczne wymagania:
WHERE new_col IS NULL)Domyślny cel rollbacku: szybko przywrócić kompatybilność aplikacji, nawet jeśli dane nie zostaną idealnie cofnięte.
Działający plan rollback powinien zawierać:
Często najbezpieczniejszy rollback to przełączenie odczytów z powrotem na stare pole, pozostawiając nowe kolumny.
To zapobiega zgadywaniu i wymusza właściwą kolejność działań.
To powoduje, że backfille przetrwają realny ruch produkcyjny.