Миграции PostgreSQL без простоя: ошибки и безопасные приёмы
Изменение схемы PostgreSQL может на секунды остановить запись или надолго занять диск — даже если сама команда выглядит безобидно. Разбираем типичные ошибки и безопасные приёмы для PostgreSQL 16: от добавления столбца до индекса и ограничений.

Добавить столбец или индекс в PostgreSQL кажется делом на одну команду, но неудачная миграция способна заблокировать запись, задержать релиз или оставить после сбоя недействующий индекс. В PostgreSQL 16 безопасный подход — не искать магическую команду «без блокировок», а разбить изменение на совместимые шаги и заранее учесть, какие блокировки они берут.
Сначала проверьте совместимость релиза и схемы
Миграция — это не только SQL. Пока сервис разворачивается, в системе могут одновременно работать старые и новые версии приложения. Если удалить столбец, пока старый код ещё его читает, или сделать обязательным поле, которое новая версия заполняет не во всех запросах, база окажется в состоянии, с которым часть экземпляров приложения не умеет работать.
Для PostgreSQL 16 полезно планировать изменения по схеме expand-contract: сначала расширить схему так, чтобы с ней работали обе версии кода, затем обновить приложение, перенести данные и только после этого удалять старое. Это не исключает коротких блокировок и нагрузки, но уменьшает риск несовместимого релиза.
Термин. Expand-contract — подход, при котором схема сначала расширяется совместимыми элементами, а устаревшие удаляются отдельным поздним шагом. Например, новый столбец добавляют до релиза кода, который начнёт в него писать.
Планируйте миграцию так, будто старый и новый код какое-то время будут работать одновременно. Именно это время чаще всего и превращает простое изменение схемы в инцидент.
Четыре частые ошибки
1. Долго держать сильную блокировку
Многие команды ALTER TABLE берут блокировку, которая мешает другим операциям с таблицей. Часто это ACCESS EXCLUSIVE: она конфликтует даже с обычным чтением. Само изменение может быть быстрым, но если нужная блокировка недоступна, команда будет ждать. А получив её, способна поставить в очередь новые запросы к таблице.
Не запускайте изменение вслепую в час пик. Установите короткий lock_timeout для миграции и повторите попытку позже, если таблица занята. Не путайте его с statement_timeout: первый ограничивает ожидание блокировки, второй — общее время выполнения запроса. Тайм-аут не делает операцию безопасной сам по себе, но снижает шанс, что миграция надолго повиснет в очереди.
2. Добавить обязательный столбец одним шагом
Новая колонка с постоянным значением по умолчанию в PostgreSQL 16 обычно не требует переписывать каждую строку таблицы — такое поведение действует с PostgreSQL 11. Но команда всё равно может ждать блокировку, а значение, вычисляемое заново для каждой строки, например через volatile-функцию, способно привести к переписыванию таблицы.
Рискованнее сразу добавить колонку как NOT NULL, если таблица уже содержит строки, а приложение ещё не записывает значение. Без корректного значения существующие данные не пройдут проверку. Безопаснее разделить работу:
- Добавьте nullable-столбец без тяжёлого значения по умолчанию. Проверьте, что команда завершилась быстро и приложение продолжает читать и записывать данные.
- Разверните код, который заполняет новый столбец при каждой новой записи. Проверьте записи через все пути: API, фоновые задачи, административные инструменты и импорты.
- Заполните старые строки небольшими порциями. Между порциями контролируйте нагрузку и число строк, где значение ещё отсутствует.
- Добавьте ограничение проверки через
NOT VALID, проверьте существующие строки командойVALIDATE CONSTRAINT, затем установитеNOT NULL. Удалите временное ограничение после проверки, что оно больше не нужно.
Для PostgreSQL 16 проверенное ограничение CHECK (column_name IS NOT NULL) может позволить выполнить SET NOT NULL без повторного полного сканирования таблицы. Но сам момент переключения всё ещё требует блокировки: настройте короткий тайм-аут и проверьте документацию для конкретной команды.
3. Создать индекс обычной командой на живой таблице
CREATE INDEX может блокировать изменения таблицы до завершения построения. Для большой таблицы используйте CREATE INDEX CONCURRENTLY: команда позволяет продолжать обычные записи, но строит индекс дольше, проводит несколько проходов и может ждать завершения старых транзакций.
CREATE INDEX CONCURRENTLY orders_created_at_idx
ON orders (created_at);
В PostgreSQL 16 эту команду нельзя выполнять внутри обычного транзакционного блока. Если построение прервётся, может остаться индекс со статусом invalid: он не работает как обычный индекс, но сам факт сбоя требует проверки. Не запускайте создание повторно, не выяснив, что осталось в каталоге, и не удаляйте индекс вслепую: для удаления может подойти DROP INDEX CONCURRENTLY, у которого тоже есть ограничения на запуск.
Уникальный индекс требует ещё большей осторожности. Если в данных уже есть дубликаты, построение не завершится успешно. Перед миграцией проверьте дубли тем же набором ключей, по которому строите индекс, и отдельно спланируйте очистку конфликтующих строк.
4. Проверять ограничение только после полного блокирующего сканирования
Для внешнего ключа можно разделить добавление правила и проверку старых строк. В PostgreSQL 16 команда с NOT VALID не проверяет всю существующую таблицу при добавлении ограничения, но новые и изменяемые строки уже должны ему соответствовать. Позже старые данные проверяют отдельно.
ALTER TABLE order_items
ADD CONSTRAINT order_items_order_id_fkey
FOREIGN KEY (order_id) REFERENCES orders (id) NOT VALID;
ALTER TABLE order_items
VALIDATE CONSTRAINT order_items_order_id_fkey;
Проверка через VALIDATE CONSTRAINT снижает риск долгой блокировки по сравнению с одним шагом, но не бесплатна: она читает данные и конкурирует за ресурсы. Запускайте её с мониторингом нагрузки. Для ограничений CHECK действует похожий принцип: добавить как NOT VALID, затем проверить отдельно.
Какой приём выбрать
| Изменение | Рискованный вариант | Более безопасный план | Цена и ограничение |
|---|---|---|---|
| Новый обязательный столбец | Сразу добавить NOT NULL и ожидать, что данные «как-нибудь» появятся | Добавить nullable-колонку, обновить запись в приложении, заполнить старые строки порциями, проверить ограничение | Потребуются несколько релизных шагов и контроль заполнения |
| Индекс на большой таблице | Обычный CREATE INDEX во время активных записей | CREATE INDEX CONCURRENTLY и проверка статуса результата | Дольше строится, создаёт нагрузку, нельзя запускать в транзакционном блоке |
| Внешний ключ или проверка данных | Сразу добавить ограничение с проверкой всей таблицы | Добавить NOT VALID, затем отдельно выполнить VALIDATE CONSTRAINT | Старые данные всё равно нужно проверить; новые записи уже должны соблюдать правило |
| Переименование или удаление поля | Поменять схему и код одним релизом | Сначала поддержать старое и новое имя, перенести чтение и запись, удалить старое позже | Временная двойная логика усложняет код и требует отдельного этапа очистки |
Когда какой подход выбирать
Для столбца, который нужно заполнить данными из приложения, выбирайте несколько релизных шагов и пакетное обновление. Для индекса на крупной активно изменяемой таблице — concurrent-построение, если допустимы его длительность и нагрузка. Для проверки внешнего ключа или CHECK на большой таблице — разделение добавления и валидации.
Переименование обычно безопаснее оформлять как переход, а не как мгновенную замену: добавьте новое поле, временно синхронизируйте оба, переведите чтение, убедитесь, что старые экземпляры кода исчезли, и лишь затем удаляйте прежнее. Если схема критична для нескольких сервисов, согласуйте порядок релизов между их командами.
Что ломается и что проверить перед запуском
Самая частая скрытая причина задержек — не размер таблицы, а открытая транзакция, которая удерживает блокировку или старый снимок данных. Долгие транзакции могут мешать завершению конкурентного построения индекса. Поэтому проверяйте их до миграции и следите за очередью запросов во время неё.
- Проверьте, что резервное копирование и восстановление укладываются в ваши требования. Миграция не заменяет проверенную возможность отката.
- Уточните блокировки и ограничения именно для используемой команды в документации PostgreSQL 16. Одинаково выглядящие изменения могут иметь разную цену.
- Посмотрите на размер таблицы и индексов, свободное место на диске, интенсивность записи и длительные транзакции.
- Разверните совместимую версию приложения до удаления старого поля или изменения его смысла. Проверьте фоновые задачи и интеграции, а не только основной веб-сервис.
- После выполнения проверьте состояние индекса, число необработанных строк, ошибки приложения, задержки запросов и репликацию, если она используется.
Откат схемы не всегда означает обратную команду: новые данные могли уже попасть в новый столбец, а удаление колонки уничтожает их. До запуска решите, можно ли восстановить прежнее состояние, или нужен отдельный компенсирующий шаг.
Что делать дальше
Возьмите ближайшее изменение схемы и составьте для него план из трёх частей: что добавляется до релиза кода, что меняется после переключения трафика и что удаляется только в отдельном релизе. На тестовой копии проверьте время выполнения, блокировки и поведение при прерывании команды. В проде наблюдайте не только за длительностью миграции, но и за ожиданием блокировок, задержкой запросов, дисковым вводом-выводом и отставанием реплик.
Транскрибатор встреч: Запись созвона или диктофона — в текст с разделением по спикерам, протоколом и списком задач.
Спросить за 25 ₽Источники: PostgreSQL 16: ALTER TABLE, PostgreSQL 16: CREATE INDEX
Частые вопросы
Можно ли гарантировать миграцию PostgreSQL вообще без простоя?
Безопасно ли добавлять столбец с DEFAULT в PostgreSQL 16?
Чем опасен незавершённый CREATE INDEX CONCURRENTLY?
Можно ли запускать CREATE INDEX CONCURRENTLY в транзакции миграционного фреймворка?
Если миграция не уложилась в окно релиза, что делать?
Достаточно ли добавить внешний ключ через NOT VALID, чтобы старые данные были корректны?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.