Миграции на проде — как менять схему живой БД
Самое страшное место в работе с БД — миграция на проде. На локалке тестировал — всё ок. Запустил на проде — таблица users заблокировалась на 5 минут, сайт лёг, телефон разрывается. Через день читаешь статью «как мы потеряли 500 тысяч». В этом уроке — какие миграции безопасны, какие опасны, и как катить опасные без даунтайма.
Что вообще такое миграция
Миграция — это файл с SQL, который переводит схему из состояния A в состояние B. Prisma genertes их автоматически: ты меняешь schema.prisma, запускаешь
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М строк × сложность = минуты). Четвёртое: окно деплоя. Если ожидается даунтайм — выкатывай ночью или предупреди юзеров. Пятое: план отката. Если миграция упала на проде — что делать? Восстановление из бэкапа или обратная миграция.
-- ❌ ОПАСНО — заблокирует таблицу на минуты
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. Никто не пострадалОпасная миграция и её безопасный вариант