Блокировка по расписанию: REFRESH MATERIALIZED VIEW
История о блокировках обычного REFRESH MATERIALIZED VIEW и требованиях к безопасному обновлению с CONCURRENTLY.
КейсPostgreSQLБлокировкиМатериализованные представленияАлерт: «Блокировки в prod-db-sigma: 27 сессий ждут AccessExclusiveLock более 10 минут».
Макс потянулся за кофе, но, увидев цифры, передумал. В его голове тут же всплыли возможные причины:
-
Долгая миграция? Нет, сегодня релизов не было.
-
Кто-то забыл транзакцию? Проверим.
Запустив типовой запрос он увидел виновника, который держал всех в заложниках: “REFRESH MATERIALIZED VIEW some_analytics_view;”, запущенный из pg_cron scheduler.
Макс мысленно вздохнул:
“Ну конечно, матвью. Почему-то все забывают, что обычный REFRESH блокирует ВСЁ, как будто это не 2025 год, и будто мы еще на постгре версии 12.”
Набросав в общий чат: “Занимаюсь блокировкой в prod-db-sigma”, он начал действовать.
Почему нельзя просто добавить CONCURRENTLY
Для REFRESH MATERIALIZED VIEW CONCURRENTLY нужен хотя бы один UNIQUE-индекс, который использует только имена столбцов, охватывает все строки и не является partial или expression index. Представление также должно быть уже заполнено, а одновременно может выполняться только один REFRESH этой materialized view.
Макс проверил есть ли индексы:
SELECT indexname, indexdefFROM pg_indexesWHERE schemaname = 'analytics' AND tablename = 'some_analytics_view';Результат: “0 rows” — вот и ответ.
Убедившись, что набор столбцов действительно уникален, Макс согласовал отмену блокирующего refresh и проверил состояние задания в pg_cron. Прежде чем создавать индекс, он отдельно проверил дубли: ошибиться в предположении об уникальности посреди инцидента было бы особенно неприятно.
Затем запустил создание индекса:
CREATE UNIQUE INDEX CONCURRENTLY idx_some_analytics_view_idON analytics.some_analytics_view (id);Пока создавался индекс, Макс обновил задание в pg_cron с ключевым словом CONCURRENTLY. Индекс тем временем уже был создан, благо матвью была небольшой. Теперь уже можно было проверить свое предположение, запустив правильный рефреш:
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.some_analytics_view;Убедившись, что читатели больше не ждут эксклюзивную блокировку, Макс проверил прогресс и нагрузку. CONCURRENTLY сохраняет доступность чтения, но обычно требует больше работы и всё равно использует блокировки на отдельных фазах.
После отмены задания нужно было проверить очередь запусков: pg_cron не запускает несколько экземпляров одной задачи одновременно, но пропущенные по расписанию запуски могут ожидать завершения предыдущего. Исправить SQL — полдела; важно убедиться, что расписание и история запусков вернулись в ожидаемое состояние.
А впереди оставались еще более скучные дела:
-
Оформить тикет: «Какой ужасный день для этого…».
-
Добавить запись в базу знаний: «Напоминание: materialized view без подходящего уникального индекса нельзя обновлять с
CONCURRENTLY». -
Найти владельца процесса и провести короткий разбор.
Спустя час Макс откинулся в кресле, глядя на зеленые графики в мониторинге. Очередной пожар потушен, база знаний пополнена, разработчик проинформирован.
“Ну вот, теперь можно и кофе допить… Хотя стоп, что это за новый алерт?..”