Programování

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.

12. 10. 2026 · 4 min čtení

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í ALTER na 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í:

  1. Expand – přidejte nové (sloupec, tabulku) tak, aby staré dál fungovalo.
  2. Migrate – kód zapisuje do obojího, data se převedou.
  3. 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

  1. Funguje stará verze kódu s novým schématem?
  2. Drží některá operace dlouhý exkluzivní zámek?
  3. Je nastavený lock_timeout?
  4. Jsou velké úpravy dat rozdělené do dávek?
  5. 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.