Добавление пустой колонки часто считают безопасной миграцией: данные не переписываются, таблица не пересоздаётся. Но в PostgreSQL даже такой ALTER TABLE может остановить новые чтения таблицы — причём до того, как структура реально изменится. Причина не в самой миграции, а в старой транзакции, которую забыли завершить. Разберём цепочку на примере и покажем, как её избежать.
Сценарий простой. В первой сессии выполняется BEGIN и SELECT id FROM orders, после чего транзакция остаётся открытой. Сам запрос уже завершён, но транзакция продолжает удерживать AccessShareLock. Это не мешает другому обычному SELECT: такие блокировки совместимы. Проверка до запуска миграции возвращает строку без ожидания.
Затем вторая сессия запускает ALTER TABLE orders ADD COLUMN note text. Для этого изменения нужна AccessExclusiveLock, несовместимая с блокировкой первой сессии. Миграция встаёт в очередь. Если теперь третья сессия выполнит SELECT id FROM orders, она тоже будет ждать. Её AccessShareLock совместим с уже выданной блокировкой первой сессии, однако впереди стоит запрос миграции на исключительный доступ.
Как выглядит очередь блокировок
В pg_locks видна вся цепочка. Сессия old_tx находится в состоянии idle in transaction и держит выданную блокировку: granted = t. У migration блокировка ещё не выдана, а blocked_by указывает на old_tx. Новый запрос api также имеет granted = f, но его blocked_by указывает уже на migration. Источник ожидания и непосредственный блокировщик запроса — разные сессии. После ROLLBACK в old_tx миграция завершается, а новый SELECT получает результат.
Этот пример показывает важное: очередь блокировок в PostgreSQL не всегда линейна с точки зрения «кто кого блокирует». Запрос может ждать не того, кто держит блокировку, а того, кто стоит перед ним в очереди. Поэтому при диагностике недостаточно смотреть только на активные запросы — нужно проверять состояние транзакций и очередь ожидания.
Что делать до запуска миграции
- Проверьте открытые транзакции: SELECT pid, state, query, xact_start FROM pg_stat_activity WHERE state = 'idle in transaction';. Если такие сессии есть, выясните, чьи они, и завершите их корректно.
- Посмотрите, кто держит блокировки на целевую таблицу: SELECT * FROM pg_locks WHERE relation = 'orders'::regclass;. Сопоставьте pid с pg_stat_activity.
- Оцените очередь ожидания: SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock';. Если в очереди уже есть запросы, миграция может встать за ними.
- Запускайте ALTER TABLE с lock_timeout, например SET lock_timeout = '3s';. Это не ускорит миграцию, но не даст ей бесконечно ждать и блокировать новые чтения.
- Для больших таблиц используйте CREATE INDEX CONCURRENTLY и другие неблокирующие приёмы, где это применимо. Но помните: ADD COLUMN с AccessExclusiveLock всё равно требует короткой блокировки.
Типичные ошибки
- Считать, что «пустая колонка» не блокирует чтения. Блокировка берётся на уровне таблицы, а не данных.
- Оставлять транзакции в состоянии idle in transaction после SELECT в psql или в приложении. Это частая причина неожиданных блокировок.
- Запускать миграцию без lock_timeout и без предварительной проверки pg_stat_activity. В результате ALTER TABLE ждёт, а за ним ждут все новые SELECT.
- Игнорировать очередь: если миграция уже ждёт, новые запросы встают за ней, даже если совместимы с текущими блокировками.
- Завершать сессии через pg_terminate_backend без понимания последствий. Это может прервать полезную работу.
Практический порядок действий
Перед миграцией выполните три шага. Первый: проверьте pg_stat_activity на предмет idle in transaction и долгих транзакций. Второй: проверьте pg_locks на таблицу, которую собираетесь менять. Третий: установите lock_timeout и запускайте ALTER TABLE. Если миграция не получила блокировку за отведённое время, она завершится с ошибкой, а не будет держать очередь. После ошибки разберитесь, кто держит блокировку, и повторите.
Пример: в сессии A выполнен BEGIN и SELECT, сессия осталась открытой. Сессия B запускает ALTER TABLE ADD COLUMN. Сессия C выполняет SELECT и ждёт. Если в сессии B был lock_timeout = '3s', то через 3 секунды B завершится с ошибкой, и C получит результат. Без lock_timeout B будет ждать бесконечно, а C — за B. Разница только в одной настройке.
Ещё один нюанс: в PostgreSQL 17 поведение ADD COLUMN для пустой колонки оптимизировано, но это не отменяет требования AccessExclusiveLock. Поэтому даже «мгновенная» миграция может встать в очередь. Проверяйте версию и тестируйте на стенде, но не полагайтесь на то, что конкретная версия «не блокирует». Блокировка нужна для изменения каталога, и её совместимость с другими блокировками не меняется.
В итоге правило простое: перед любой миграцией смотрите не только на SQL, но и на состояние транзакций и очередь блокировок. Забытый BEGIN — частая причина, но не единственная. Долгие запросы, открытые курсоры, приложения без autocommit — всё это может держать AccessShareLock. Проверка занимает минуту, а предотвращает простой чтений.