intDataBlog
← ЖурналМаршруты обучения ↗

Миграции PostgreSQL: менять схему без долгой блокировки

Расширение схемы, совместимые версии приложения, поэтапное заполнение и отдельная проверка ограничений.

Изменение схемы на пустой базе занимает мгновение. На большой таблице та же команда может ждать чужую транзакцию, переписывать данные или удерживать блокировку дольше, чем приложение готово терпеть. Короткий SQL-файл ещё не означает короткое воздействие на сервис.

У разных форм ALTER TABLE различаются блокировки и объём работы. PostgreSQL позволяет добавлять некоторые ограничения как NOT VALID, а проверять существующие строки отдельной командой VALIDATE CONSTRAINT. Новые изменения при этом уже должны удовлетворять добавленному ограничению. PostgreSQL 18: ALTER TABLE

Разделить изменение на этапы

Пусть в учебном каталоге вместо одного поля display_name нужны given_name и family_name. Переименование столбца не решает задачу: старый экземпляр приложения продолжит искать прежнее имя, а автоматическое разделение строки не гарантирует корректных данных.

Сначала схема расширяется новыми необязательными полями. Следующая версия приложения умеет читать обе формы и сохраняет согласованное представление по выбранному правилу. После этого данные заполняются порциями с явным учётом неоднозначных строк. Только когда старые версии перестали использовать прежний контракт, можно обсуждать его удаление.

Это последовательность совместимости, а не обещание миграции без риска. Нужно определить, что происходит с записью, которую пользователь изменил во время заполнения. Один вариант — условное обновление лишь строк, где новые поля ещё пусты; другой — версия записи и проверка, что исходные данные не менялись. Выбор зависит от смысла данных.

Ограничение отдельно от большого сканирования

Пример с учебным полем amount:

BEGIN;
SET LOCAL lock_timeout = '1s';
ALTER TABLE demo_orders
  ADD CONSTRAINT demo_orders_amount_check
  CHECK (amount >= 0) NOT VALID;
COMMIT;

-- Отдельный этап, после разбора старых нарушений:
ALTER TABLE demo_orders
  VALIDATE CONSTRAINT demo_orders_amount_check;

CHECK в этом виде допускает NULL: выражение с неизвестным результатом не считается нарушением. Если бизнес-контракт запрещает NULL, это нужно задать отдельно. Учебная таблица предполагается существующей; команды не являются готовой миграцией произвольной рабочей базы.

Значение lock_timeout здесь иллюстрирует предпочтение быстрого контролируемого отказа перед неожиданным долгим ожиданием. Оно не универсально. План миграции должен описывать, кто повторяет неудавшийся этап и как убеждается, что предыдущая попытка не оставила частичный результат.

Индекс CONCURRENTLY тоже требует плана

CREATE INDEX CONCURRENTLY уменьшает блокирование записи по сравнению с обычным построением, но выполняет больше работы, может ждать другие транзакции и при сбое оставлять невалидный индекс. Эту команду нельзя выполнять внутри обычного блока транзакции. PostgreSQL 18: CREATE INDEX

Поэтому перед запуском нужно проверить поведение инструмента миграций: не оборачивает ли он весь файл в BEGIN/COMMIT. После запуска проверяют состояние конкретного индекса, а не только отсутствие ошибки у внешнего процесса. На репликах отдельно наблюдают отставание и нагрузку.

Как принять миграцию

Для учебного изменения полезна матрица из двух версий приложения и двух состояний схемы. Старое приложение должно пережить расширение, новое — работать до завершения заполнения, повторный запуск этапа — не портить уже обработанные данные.

Перед финальным удалением старого столбца проверяют реальные обращения: фоновые задания, отчёты, выгрузки и редкие административные команды. Один успешный веб-сценарий не доказывает, что старый контракт больше никому не нужен.

Для каждой стадии заранее определяют условия остановки: ожидание блокировки, рост задержки, нарушение данных, превышение допустимого отставания. Возврат предыдущей версии приложения возможен лишь пока схема и данные с ней совместимы. Файл с командой обратного переименования сам по себе такой совместимости не обеспечивает.