VACUUM и autovacuum

VACUUM — это уборщик PostgreSQL: он возвращает в оборот место от мёртвых кортежей, а autovacuum делает это за вас в фоне — если ему не мешать и правильно настроить.

VACUUM — операция очистки, которая помечает занятое мёртвыми версиями место как свободное для повторного использования и обновляет служебные карты. Autovacuum — фоновый демон, который запускает VACUUM и ANALYZE автоматически по порогам изменений.

В прошлом уроке мы научились видеть мёртвые кортежи и bloat. Теперь — как с ними бороться штатно. PostgreSQL не чистит мусор сам в момент UPDATE: уборка вынесена в отдельный процесс, чтобы не замедлять запись. Понимать VACUUM важно, потому что 90% проблем с раздуванием — это не «VACUUM не работает», а «autovacuum не успевает» или «ему мешают».

Зачем это на практике

Грамотно настроенный autovacuum — это разница между базой, которая годами работает ровно, и базой, которая раз в квартал «внезапно» раздувается и требует аварийного обслуживания. Кроме уборки мусора VACUUM решает ещё одну критическую задачу — заморозку версий для защиты от переполнения счётчика транзакций (wraparound), который при игнорировании способен остановить запись в базу целиком. Плюс ANALYZE кормит планировщик свежей статистикой, без которой он строит плохие планы. Эти три вещи — выживание, скорость и корректность планов — стоят на обслуживании.

Что именно делает VACUUM

Обычный VACUUM (без FULL) выполняет несколько задач, не блокируя обычную работу с таблицей:

  • Освобождает мёртвые версии: помечает место, занятое мёртвыми кортежами, как свободное — оно будет переиспользовано будущими вставками в этой же таблице.
  • Чистит индексы: удаляет из индексов ссылки на убранные версии.
  • Обновляет карту видимости и карту свободного места — это ускоряет index-only scan и подсказывает, куда класть новые строки.
  • Замораживает старые версии (freeze), сдвигая горизонт wraparound и защищая базу от остановки.

Важно: обычный VACUUM не отдаёт место операционной системе. Файл таблицы остаётся прежнего размера, просто внутри появляется переиспользуемое свободное пространство. Для приложения этого достаточно — рост прекращается.

VACUUM orders;                 -- обычная уборка, без блокировки таблицы
VACUUM (VERBOSE) orders;       -- с отчётом: сколько версий убрано, сколько страниц
VACUUM (ANALYZE) orders;       -- уборка + пересбор статистики одной командой

VACUUM FULL против обычного VACUUM

Когда таблица уже сильно раздута и место надо вернуть диску, обычного VACUUM мало. VACUUM FULL переписывает таблицу заново в компактный файл без пустот.

VACUUM (обычный)VACUUM FULL
Блокировкане мешает SELECT/INSERT/UPDATEэксклюзивная: таблица недоступна целиком
Место на дискепереиспользуется внутри файлавозвращается ОС (файл сжимается)
Как работаетправит существующий файл на местепишет новую копию + новый файл, нужен запас места
Когда применятьштатно, постоянноразово, при сильном bloat, в окно простоя
VACUUM FULL orders;   -- ЭКСКЛЮЗИВНАЯ блокировка: на проде только в окно обслуживания!

Поскольку VACUUM FULL кладёт эксклюзивную блокировку, на живом сервере его избегают. Для онлайнового устранения bloat без долгой блокировки чаще берут расширение pg_repack или перестроение индексов через REINDEX ... CONCURRENTLY.

ANALYZE: статистика для планировщика

VACUUM убирает мусор, а ANALYZE — отдельная задача: он собирает статистику о данных (сколько строк, как распределены значения, доля NULL, частые значения) в pg_statistic. Планировщик по ней оценивает, сколько строк вернёт условие, и выбирает между seq scan и index scan, порядком соединений и т.д. Устаревшая статистика — прямая причина «вдруг тормозящих» запросов после массовой загрузки данных.

ANALYZE orders;                       -- пересобрать статистику по всей таблице
ANALYZE orders (status, created_at);  -- только по конкретным столбцам

-- посмотреть, когда последний раз собиралась статистика
SELECT relname, last_analyze, last_autoanalyze FROM pg_stat_user_tables;

После большой загрузки (COPY, bulk insert) полезно вызвать ANALYZE вручную, не дожидаясь autovacuum, иначе первые запросы пойдут по плану, рассчитанному на старый объём.

Autovacuum и его настройка

Демон autovacuum периодически проверяет таблицы и запускает уборку/анализ, когда накопилось достаточно изменений. Порог срабатывания VACUUM считается по формуле:

порог = autovacuum_vacuum_threshold
      + autovacuum_vacuum_scale_factor * число_строк_в_таблице

-- значения по умолчанию:
autovacuum_vacuum_threshold     = 50
autovacuum_vacuum_scale_factor  = 0.2     -- 20% строк
autovacuum_analyze_scale_factor = 0.1     -- 10% строк для ANALYZE

То есть по умолчанию таблица вакуумируется, когда изменилось ~20% строк. Для маленькой таблицы это нормально, а для таблицы в 100 млн строк 20% — это 20 млн мёртвых версий до первой уборки: слишком поздно. Поэтому для крупных горячих таблиц scale_factor уменьшают персонально:

-- агрессивнее убирать конкретную большую таблицу
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor  = 0.02,   -- 2% вместо 20%
  autovacuum_vacuum_threshold     = 1000,
  autovacuum_analyze_scale_factor = 0.01
);

Другой частый тюнинг — скорость уборки. Autovacuum намеренно «спит» между порциями работы (cost-based delay), чтобы не грузить диск. На быстрых дисках это делает его слишком медленным, и он не успевает за нагрузкой; тогда поднимают лимит работы и убирают паузу:

-- в postgresql.conf — сделать autovacuum расторопнее
autovacuum_vacuum_cost_limit = 2000   -- больше 'бюджета' на один проход (по умолчанию 200)
autovacuum_vacuum_cost_delay = 2ms    -- меньше пауза (в новых версиях по умолчанию уже 2ms)
autovacuum_max_workers       = 5      -- больше параллельных уборщиков

Как это работает под капотом

Заморозка (freeze) — не просто «уборка про запас». Счётчик транзакций xid 32-битный и закольцован. Чтобы старые версии не «оказались из будущего» после прохождения круга, их xmin помечается как «заморожен» — то есть видим всем. VACUUM обязан успевать замораживать версии раньше, чем счётчик пройдёт критическую дистанцию (autovacuum_freeze_max_age, по умолчанию 200 млн). Если уборка хронически отстаёт, PostgreSQL сначала запускает «агрессивный» антивраппинговый autovacuum (его нельзя отключить порогами таблицы), а в крайнем случае переходит в режим, где запрещает новые транзакции, пока администратор не выполнит уборку. Поэтому отключать autovacuum «чтобы не мешал» — опасная идея: можно довести базу до полной остановки записи.

Частые ошибки

  • Отключать autovacuum. «Он мешает нагрузке» — и через недели база раздута, а счётчик транзакций у опасной черты. Почти всегда правильнее настроить, а не выключить.
  • Гонять VACUUM FULL на проде по расписанию. Эксклюзивная блокировка останавливает работу с таблицей; для онлайна есть pg_repack.
  • Оставлять scale_factor 0.2 для огромных таблиц. Уборка приходит слишком поздно; крупным таблицам нужен индивидуальный, заметно меньший порог.
  • Забывать ANALYZE после массовой загрузки. Статистика устаревает, планировщик ошибается, запросы тормозят, хотя данные и индексы в порядке.
  • Душить autovacuum слишком сильным cost-delay. На быстрых дисках демон еле ползёт и не успевает за потоком изменений.

Итоги

  • Обычный VACUUM освобождает место от мёртвых версий для переиспользования, чистит индексы, обновляет карты и замораживает версии — не блокируя работу и не отдавая место ОС.
  • VACUUM FULL переписывает таблицу, возвращая место диску, но берёт эксклюзивную блокировку — только в окно простоя.
  • ANALYZE собирает статистику для планировщика; после bulk-загрузки его полезно вызвать вручную.
  • Autovacuum запускается по порогу threshold + scale_factor * строки; для больших горячих таблиц scale_factor снижают индивидуально через ALTER TABLE.
  • Заморозка защищает от wraparound; отключать autovacuum опасно — база может остановить запись.
Проверьте себя
1. Чем обычный VACUUM отличается от VACUUM FULL?
AОбычный VACUUM возвращает место ОС, а VACUUM FULL — нет
BОбычный VACUUM помечает место свободным для переиспользования внутри файла и не блокирует таблицу, а VACUUM FULL переписывает таблицу, возвращает место ОС, но берёт эксклюзивную блокировку
CЭто синонимы, отличается только синтаксис
DОбычный VACUUM работает только с индексами, а VACUUM FULL — только с кучей
2. Зачем нужен ANALYZE отдельно от VACUUM?
AЧтобы физически удалить мёртвые кортежи
BЧтобы собрать статистику о распределении данных, по которой планировщик выбирает планы запросов
CЧтобы заблокировать таблицу на время загрузки
DЧтобы перестроить все индексы таблицы
3. Что произойдёт с большой горячей таблицей при autovacuum_vacuum_scale_factor по умолчанию (0.2)?
AУборка будет идти после каждого UPDATE
BУборка запустится только когда изменится около 20% строк, что для очень большой таблицы означает миллионы мёртвых версий и запоздалую очистку
CAutovacuum вообще не будет её трогать
DТаблица будет вакуумироваться каждую секунду независимо от изменений