Короткий ответ
Миграцию обычно разбивают на несколько независимых, обратно совместимых шагов, каждый из которых можно выкатить и при необходимости откатить отдельно, не требуя одновременной остановки приложения и базы.
Как это работает подробнее
Классический подход — expand/contract: сначала добавляют новую колонку как необязательную (expand), выкатывают код, который пишет в оба места (старое и новое) или постепенно переключается на новое, затем переносят/бэкфилят исторические данные, и только после полного перехода удаляют старую колонку или делают новую обязательной (contract). Между шагами приложение всегда работает с совместимой схемой, а не ждёт единомоментного переключения.
Пример кода
migration.sql
-- Шаг 1 (expand): добавляем колонку как nullable, не блокируя таблицу
ALTER TABLE orders ADD COLUMN currency VARCHAR(3) NULL;
-- Шаг 2: бэкфилл батчами в фоне
UPDATE orders SET currency = 'RUB'
WHERE currency IS NULL AND id BETWEEN 1 AND 10000;
-- повторять для следующих диапазонов id
-- Шаг 3 (contract, после полного бэкфилла и перехода кода):
ALTER TABLE orders ALTER COLUMN currency SET NOT NULL;Что использовать на практике
- Практика. Важно учитывать порядок выката: код должен уметь работать как со старой, так и с новой схемой во время переходного периода, потому что при деплое на несколько инстансов какое-то время одновременно работают старая и новая версии кода. Бэкфилл больших таблиц стоит делать батчами в фоне, чтобы не держать долгие блокировки и не создавать пиковую нагрузку на основную базу в рабочее время.
Подводные камни
- Риски. Частая ошибка — сделать миграцию одним шагом (ADD COLUMN NOT NULL без default на большой таблице), что в некоторых СУБД вызывает блокировку таблицы и простой. Другая ошибка — не предусмотреть откат: если новый код уже пишет только в новую колонку, а миграцию приходится откатывать, старые инстансы могут потерять данные или начать работать некорректно. Также часто забывают о нагрузке самого бэкфилла на реплики и мониторинг, запуская его без ограничения скорости.