EvolCode
треки/Базы данных·06 / 07
13 мин чтения

Миграции на проде — как менять схему живой БД

Самое страшное место в работе с БД — миграция на проде. На локалке тестировал — всё ок. Запустил на проде — таблица users заблокировалась на 5 минут, сайт лёг, телефон разрывается. Через день читаешь статью «как мы потеряли 500 тысяч». В этом уроке — какие миграции безопасны, какие опасны, и как катить опасные без даунтайма.

Что вообще такое миграция

Миграция — это файл с SQL, который переводит схему из состояния A в состояние B. Prisma genertes их автоматически: ты меняешь schema.prisma, запускаешь

$ prisma migrate dev --name add_email_field

Prisma сравнивает старое состояние и новое, генерирует SQL (CREATE TABLE / ALTER TABLE / etc), сохраняет в prisma/migrations/. На проде запускаешь prisma migrate deploy — все непримененные миграции применяются по порядку. История миграций живёт в таблице _prisma_migrations.

Безопасные операции — катятся без раздумий

CREATE TABLE — новая таблица, никого не блокирует. ADD COLUMN с NULL — просто добавление в metadata, мгновенно. CREATE INDEX CONCURRENTLY — индекс строится в фоне. DROP COLUMN — Postgres только переименовывает в служебной таблице, физически данные удалятся при VACUUM. Эти операции можно катить в любое время — пользователи ничего не заметят.

Опасные операции — могут положить сайт

ADD COLUMN с NOT NULL и default — Postgres перезапишет ВСЕ строки таблицы. На 10М строк это 5+ минут с EXCLUSIVE LOCK — сайт лежит. Решение: сначала ADD COLUMN nullable, в коде учим работать с NULL, потом отдельной миграцией постепенно заполняем default через UPDATE батчами по 10к, в конце — ALTER COLUMN SET NOT NULL. ALTER TYPE (изменение типа колонки) — тоже перепись всех строк. Решение: новая колонка → копирование данных → переключение кода → удаление старой.

Расширяй, не ломай — главный принцип миграций

Старая версия кода и новая версия кода работают одновременно во время деплоя (rolling deploy). Это значит: миграция должна быть совместима с обеими версиями кода. Не удаляй колонку которой пользуется старый код — сначала задеплой код без неё, потом удали в следующей миграции. Не переименовывай колонку — добавь новую, скопируй данные, перейди на неё, в третьей миграции удали старую. Эта последовательность называется expand-contract pattern: расширяем (добавили новое), потом сжимаем (удалили старое после деплоя нового кода).

Чек-лист перед запуском миграции на проде

Первое: бэкап. Перед любой нетривиальной миграцией — pg_dump или snapshot в Neon (одна кнопка). Второе: прогон на staging с реалистичным размером данных, не на пустой тестовой БД. Третье: оценка времени. Если миграция перепишет всю таблицу — посчитай сколько займёт на проде (примерно: 10М строк × сложность = минуты). Четвёртое: окно деплоя. Если ожидается даунтайм — выкатывай ночью или предупреди юзеров. Пятое: план отката. Если миграция упала на проде — что делать? Восстановление из бэкапа или обратная миграция.

примерsql
-- ❌ ОПАСНО — заблокирует таблицу на минуты
ALTER TABLE users
  ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
-- На 10М строк: EXCLUSIVE LOCK, переписать каждую строку, время 5+ минут
-- Сайт во время этого: 500 ошибок на каждый запрос


-- ✅ БЕЗОПАСНЫЙ ПУТЬ (3 миграции + 1 деплой)

-- Миграция 1: добавить nullable, мгновенно
ALTER TABLE users ADD COLUMN status TEXT;

-- Деплой кода: учим работать с status === null
-- (трактуем NULL как 'active' в коде до полного бэкфилла)

-- Миграция 2: бэкфилл батчами, не блокирует
DO $$
DECLARE
  rows_updated INT;
BEGIN
  LOOP
    UPDATE users
    SET status = 'active'
    WHERE status IS NULL
      AND id IN (
        SELECT id FROM users WHERE status IS NULL LIMIT 10000
      );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;
    PERFORM pg_sleep(0.1);  -- даём другим запросам подышать
  END LOOP;
END $$;

-- Миграция 3: теперь NOT NULL — мгновенно (все строки уже не-NULL)
ALTER TABLE users
  ALTER COLUMN status SET NOT NULL,
  ALTER COLUMN status SET DEFAULT 'active';


-- В Prisma тот же подход:
-- 1. Добавляешь поле как String? в schema.prisma
-- 2. prisma migrate dev → катишь nullable версию
-- 3. Скрипт-бэкфилл (через prisma.user.updateMany по партиям)
-- 4. Меняешь на String (без ?), prisma migrate dev → второй раунд
-- 5. Никто не пострадал

Опасная миграция и её безопасный вариант

главное
  • 01Миграция = SQL который переводит БД из старой схемы в новую. Версионируется в Git
  • 02Безопасные: CREATE TABLE, ADD COLUMN nullable, CREATE INDEX CONCURRENTLY
  • 03Опасные: ADD COLUMN с NOT NULL+default, ALTER TYPE, DROP TABLE с FK — переписывают данные с локом
  • 04Expand-contract: сначала расширь (добавь новое), задеплой код, потом сожми (удали старое)
  • 05Бэкфилл больших таблиц — батчами по 5-10к строк, с pg_sleep между ними. Иначе — лок
  • 06Перед нетривиальной миграцией: бэкап + прогон на staging с реалистичным объёмом данных
  • 07План отката обязателен. Что делать если миграция упала на 50% — реальный сценарий
проверь себя
01Что произойдёт если на таблице с 10М юзеров запустить `ALTER TABLE users ADD COLUMN tier TEXT NOT NULL DEFAULT "free"`?
02Зачем нужен expand-contract pattern при переименовании колонки?
03Перед миграцией на проде — что обязательный шаг #1?
следующий урок: Branching БД — Git-подход к данным