Материалы / Истории / Блокировка по расписанию: REFRESH MATERIALIZED VIEW
История

Блокировка по расписанию: REFRESH MATERIALIZED VIEW

История о блокировках обычного REFRESH MATERIALIZED VIEW и требованиях к безопасному обновлению с CONCURRENTLY.

Алерт: «Блокировки в 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, indexdef
FROM pg_indexes
WHERE schemaname = 'analytics'
AND tablename = 'some_analytics_view';

Результат: “0 rows” — вот и ответ.

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

Затем запустил создание индекса:

CREATE UNIQUE INDEX CONCURRENTLY idx_some_analytics_view_id
ON analytics.some_analytics_view (id);

Пока создавался индекс, Макс обновил задание в pg_cron с ключевым словом CONCURRENTLY. Индекс тем временем уже был создан, благо матвью была небольшой. Теперь уже можно было проверить свое предположение, запустив правильный рефреш:

REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.some_analytics_view;

Убедившись, что читатели больше не ждут эксклюзивную блокировку, Макс проверил прогресс и нагрузку. CONCURRENTLY сохраняет доступность чтения, но обычно требует больше работы и всё равно использует блокировки на отдельных фазах.

После отмены задания нужно было проверить очередь запусков: pg_cron не запускает несколько экземпляров одной задачи одновременно, но пропущенные по расписанию запуски могут ожидать завершения предыдущего. Исправить SQL — полдела; важно убедиться, что расписание и история запусков вернулись в ожидаемое состояние.

А впереди оставались еще более скучные дела:

  • Оформить тикет: «Какой ужасный день для этого…».

  • Добавить запись в базу знаний: «Напоминание: materialized view без подходящего уникального индекса нельзя обновлять с CONCURRENTLY».

  • Найти владельца процесса и провести короткий разбор.

Спустя час Макс откинулся в кресле, глядя на зеленые графики в мониторинге. Очередной пожар потушен, база знаний пополнена, разработчик проинформирован.

“Ну вот, теперь можно и кофе допить… Хотя стоп, что это за новый алерт?..”