На что обращать внимание при добавлении колонки в таблицу и миграции
Что проверяют
Понимаете ли вы, что миграция выполняется на живой базе под нагрузкой и рядом со старым работающим кодом. Вопрос отделяет тех, кто добавлял колонку только на пустой локальной базе, от тех, кто выкатывал изменение на таблицу в десятки миллионов строк.
Как отвечать
Разложить на три части: блокировки, данные, совместимость.
Блокировки. ALTER TABLE берёт самую строгую блокировку и встаёт в очередь за любой долгой транзакцией, а за ним встают все остальные запросы, включая чтения. Поэтому ставим короткий lock_timeout и повторяем попытку, а не ждём молча. Само добавление колонки в PostgreSQL начиная с 11 версии таблицу не переписывает даже с DEFAULT, если значение не волатильное.
Данные. Колонку заводим nullable, без NOT NULL и без вычисляемого значения. Заполняем пачками по ключу, с паузами, повторяя до нуля обновлённых строк. Ограничение добавляем потом: в PostgreSQL через NOT VALID и отдельный VALIDATE CONSTRAINT, который берёт слабую блокировку. Индекс строим через CREATE INDEX CONCURRENTLY, вне транзакции, в Alembic и Django для этого нужен неатомарный режим миграции.
Совместимость. Выкатка в два шага: сначала схема, потом код. Старые экземпляры приложения должны пережить новую колонку, значит SELECT * и INSERT без явного списка колонок запрещены. Откат тоже пишется и проверяется.
Где спотыкаются
Оборачивают всё в одну транзакцию «ради атомарности». В PostgreSQL DDL действительно транзакционный, но длинная транзакция всё это время держит блокировку на таблицу; в MySQL операция коммитится сама, и откатить её транзакцией не выйдет.
Второе: бэкофилл одним UPDATE на всю таблицу. Он переписывает каждую строку целиком, раздувает файлы, мешает автовакууму и часто просто не доезжает до конца.
Третье: проверяют миграцию на пустой локальной базе. Там любой ALTER мгновенный, а на проде тот же скрипт останавливает сервис.