Почему CREATE INDEX CONCURRENTLY ждёт
История о фазах конкурентного создания индекса, старых транзакциях и INVALID-индексе после прерванной операции.
КейсPostgreSQLИндексыБлокировкиВ кабинет к Максу заглянул Вася с лицом человека, который только что проиграл схватку с реальностью:
— Макс, объясни. Я же делаю CREATE INDEX CONCURRENTLY, он не должен никого блокировать… а он висит уже второй час для маленькой таблицы!
Макс даже не повернул голову от монитора:
— Так он и не блокирует “как обычный” CREATE INDEX. В твоем случае он скорее всего ждет завершения какой-то транзакции. CONCURRENTLY — это не “вне законов физики”. Он строит индекс в несколько фаз и в ключевых точках обязан дождаться транзакций или снапшотов, которые ещё могут видеть старую картину данных. Поэтому какой-нибудь долгий SELECT (особенно в явной транзакции) легко превращается в шлагбаум для создания индекса.
Вася прищурился:
— Подожди… SELECT же не держит блокировку на таблицу!
— Не держит. Он просто держит твою надежду на быстрый релиз… Это не “жёсткая” блокировка таблицы, а ожидание, пока закончатся старые транзакции. В CONCURRENTLY это нормальный механизм корректности: база должна убедиться, что индекс не пропустит строки, которые кто-то ещё может видеть/менять в “старом мире”.
Макс вздохнул и сначала посмотрел фазу операции и текущую ожидаемую сессию:
SELECT progress.pid AS index_pid, progress.phase, progress.lockers_total, progress.lockers_done, progress.current_locker_pid, blocker.state AS blocker_state, now() - blocker.xact_start AS blocker_xact_age, blocker.application_name, blocker.queryFROM pg_stat_progress_create_index AS progressLEFT JOIN pg_stat_activity AS blocker ON blocker.pid = progress.current_locker_pidWHERE progress.command = 'CREATE INDEX CONCURRENTLY';pg_blocking_pids() полезна для обычных конфликтов блокировок, но фазы ожидания старых транзакций и снимков удобнее начинать с pg_stat_progress_create_index. Не каждая фаза заполняет current_locker_pid, поэтому затем нужно сопоставить phase, возраст транзакций и wait_event самой индексирующей сессии.
Вася посмотрел на вывод, и выражение лица стало ещё грустнее:
— Блокировщик… это же мой SELECT из отчета, который я запустил с утра для проверки.
— Поздравляю, — усмехнулся Макс. — Ты стал жертвой самого сложного врага — себя.
Вася нахмурился:
— И что мне теперь делать? Убить сессию с CREATE INDEX?
— Сначала выясни, можно ли безопасно завершить отчёт и его транзакцию, — Макс откинулся в кресле. — Одного окончания SQL-оператора может быть недостаточно, если приложение оставит явную транзакцию открытой. Не убивай чужие сессии без владельца и оценки отката.
— Но это же неудобно! — возмутился Вася. — Я блокирую создание индекса! Может, лучше прибью сессию с индексом, а потом заново запущу?
Макс покачал головой:
— Вот этого как раз делать не стоит. Если оборвёшь CREATE INDEX CONCURRENTLY, postgres оставит индекс в списке, но пометит его как INVALID — неполный и ненадёжный. Такой индекс не будет использоваться при выполнении запросов, но продолжит занимать место на диске и создавать накладные расходы при вставках и обновлениях.
— То есть получится “мёртвый груз”? — уточнил Вася.
— Именно. После проверки pg_index.indisvalid такой индекс придётся либо удалить через DROP INDEX CONCURRENTLY, либо перестроить подходящим способом. Гораздо проще заранее контролировать долгие транзакции и дать операции завершиться, если ожидание приемлемо.
Вася задумался:
— Понял. А как этого можно избежать в будущем?
Макс пожал плечами:
— Создание индексов, даже CONCURRENTLY, лучше планировать: заранее проверить долгие транзакции, запас диска, WAL и репликацию, поставить наблюдение за прогрессом и определить критерии отмены. Полного простоя обычно не нужно, но период низкой нагрузки заметно снижает риск.
Вася вздохнул:
— То есть CONCURRENTLY — это не волшебная кнопка “без последствий”.
— Ага, — усмехнулся Макс. — CONCURRENTLY — это как ремонт дороги без полного перекрытия движения. Машины едут, но приходится ждать, пока проедет встречная колонна.