Databázové migrace bez výpadku: expand/contract krok za krokem
Jak měnit schéma produkční databáze bez odstávky: verzované migrace, vzor expand/contract, přejmenování sloupce, NOT NULL, indexy a zámky v PostgreSQL.
Migrace jsou verzované skripty, které mění schéma databáze – stejně jako Git verzuje kód. Na lokálním počítači proběhnou za vteřinu. Na produkci s miliony řádků a běžícími požadavky ale může nevinné ALTER TABLE zamknout tabulku na minuty a shodit aplikaci. Tady je postup, jak se tomu vyhnout.
Základní pravidla migrací
- Každá změna schématu je migrace v repozitáři. Žádné ruční
ALTERna produkci. - Migrace jsou neměnné. Jakmile proběhla jinde než u vás, neupravujte ji – napište novou.
- Malé kroky. Jedna migrace, jedna logická změna.
- Spouštějte je automaticky při nasazení, ve stejném pořadí všude.
Nástroje: Laravel (php artisan migrate), Django (manage.py migrate), Rails, Alembic pro SQLAlchemy, Prisma Migrate, Flyway nebo Liquibase pro jakýkoli jazyk.
Problém: kód a schéma se nemění současně
Při nasazení běží chvíli stará i nová verze aplikace zároveň (rolling deploy, více serverů), nebo se migrace spustí dřív než nový kód. Proto každá migrace musí být kompatibilní s předchozí i následující verzí kódu.
Kdybyste sloupec přejmenovali jedním ALTER TABLE ... RENAME COLUMN, stará verze aplikace začne okamžitě padat.
Vzor expand/contract
Destruktivní změnu rozložíte do několika nasazení:
- Expand – přidejte nové (sloupec, tabulku) tak, aby staré dál fungovalo.
- Migrate – kód zapisuje do obojího, data se převedou.
- Contract – kód přestane používat staré, staré se odstraní.
Příklad: přejmenování name na full_name
Nasazení 1 – přidání sloupce:
ALTER TABLE users ADD COLUMN full_name TEXT;
Kód zapisuje do name i full_name, čte stále z name.
Nasazení 2 – doplnění dat po dávkách:
UPDATE users SET full_name = name
WHERE id IN (
SELECT id FROM users WHERE full_name IS NULL LIMIT 5000
);
Opakujte, dokud dotaz něco aktualizuje. Jeden obří UPDATE přes celou tabulku by držel zámky na řádcích dlouho, vytvořil obrovskou transakci a v PostgreSQL spoustu mrtvých řádků najednou. Po doplnění přepněte kód na čtení z full_name.
Nasazení 3 – úklid:
ALTER TABLE users DROP COLUMN name;
Až když žádná běžící verze kódu name nepoužívá.
Bezpečné a nebezpečné operace (PostgreSQL)
| Operace | Riziko | Bezpečnější postup |
|---|---|---|
ADD COLUMN bez výchozí hodnoty |
rychlé | – |
ADD COLUMN ... DEFAULT 0 |
od PG 11 rychlé (jen metadata) | ve starších verzích přepis tabulky |
ADD COLUMN ... DEFAULT now() nebo jiná volatilní hodnota |
přepis tabulky | přidat bez defaultu, doplnit po dávkách |
CREATE INDEX |
blokuje zápisy | CREATE INDEX CONCURRENTLY |
SET NOT NULL |
čte celou tabulku pod zámkem | přes CHECK ... NOT VALID (níže) |
ADD FOREIGN KEY |
kontroluje všechny řádky pod zámkem | NOT VALID + VALIDATE CONSTRAINT |
| změna typu sloupce | většinou přepis tabulky | nový sloupec + expand/contract |
NOT NULL bez dlouhého zámku
ALTER TABLE users ADD CONSTRAINT users_full_name_not_null
CHECK (full_name IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_full_name_not_null;
-- od PG 12 využije platný CHECK a nemusí tabulku procházet
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_full_name_not_null;
VALIDATE CONSTRAINT prochází tabulku, ale drží jen slabý zámek, který neblokuje čtení ani zápisy.
Cizí klíč
ALTER TABLE orders ADD CONSTRAINT orders_customer_fk
FOREIGN KEY (customer_id) REFERENCES customers(id) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_fk;
Lock timeout jako pojistka
I rychlá operace jako ADD COLUMN potřebuje na okamžik exkluzivní zámek. Pokud na tabulce běží dlouhý dotaz, ALTER čeká ve frontě – a za ním se zablokují všechny ostatní dotazy na tabulku. Nastavte proto timeout:
SET lock_timeout = '3s';
ALTER TABLE users ADD COLUMN full_name TEXT;
Když se zámek nepodaří získat, migrace selže místo shození aplikace a můžete ji zopakovat.
MySQL / MariaDB
InnoDB podporuje online DDL pro řadu operací. Explicitně si ho vyžádejte, ať víte, kdy to nejde:
ALTER TABLE users ADD COLUMN full_name VARCHAR(200), ALGORITHM=INSTANT;
ALTER TABLE users ADD INDEX idx_email (email), ALGORITHM=INPLACE, LOCK=NONE;
Pokud operace požadovaný algoritmus nepodporuje, MySQL vrátí chybu místo tichého zamčení tabulky. ALGORITHM=INSTANT pro přidání sloupce je v MySQL 8.0 (na libovolnou pozici od 8.0.29) a v MariaDB od 10.3. Pro velké tabulky a složitější změny existují nástroje pt-online-schema-change a gh-ost.
Rollback
„Down“ migrace se hodí ve vývoji, na produkci jsou ale často iluzorní – DROP COLUMN se nedá vrátit bez ztráty dat. Spolehlivější strategie:
- migrace navrhujte jako forward-only a chybu opravte další migrací,
- před rizikovou změnou udělejte zálohu,
- díky expand/contract jde většinou vrátit jen kód, schéma zůstává kompatibilní.
Kontrolní seznam před nasazením
- Funguje stará verze kódu s novým schématem?
- Drží některá operace dlouhý exkluzivní zámek?
- Je nastavený
lock_timeout? - Jsou velké úpravy dat rozdělené do dávek?
- Vyzkoušeli jste migraci na kopii produkčních dat a změřili čas?
Pokud máte server připojený v ColdHost AI přes SSH, můžete migraci nejdřív spustit na stagingu v režimu Server a DevOps a každý příkaz před spuštěním zkontrolovat a schválit.