Learning Platform
Урок 06.02 · 22 мин
Продвинутый
VACUUM FULLpg_repackpg_squeezeAccessExclusiveLockbloat

В прошлом уроке мы установили: обычный VACUUM не возвращает место операционной системе. Файл таблицы остаётся того же размера, dead tuples переезжают в LP_UNUSED, и место переиспользуется будущими INSERT’ами через FSM. Это устраивает в 95% случаев — bloat стабилизируется на каком-то уровне, размер не растёт, write-heavy таблица работает.

Но иногда нужно именно сжать таблицу: например, после массового DELETE (удалили 80% данных), после ошибочной длинной транзакции, которая «отморозила» VACUUM на неделю, или просто перед миграцией, когда хочется уехать на меньшем диске. В этих случаях у вас два варианта: VACUUM FULL или внешние инструменты pg_repack / pg_squeeze.

Как работает VACUUM FULL

VACUUM FULL table — это не «более тщательный VACUUM», как может показаться. Это совершенно другая операция:

  1. Берёт AccessExclusiveLock на таблицу — блокирует всё, включая SELECT.
  2. Создаёт новый пустой файл (новый relfilenode).
  3. Сканирует старую таблицу и копирует только живые кортежи в новый файл, упаковывая их плотно — без дыр и dead tuples.
  4. Пересоздаёт все индексы с нуля (потому что ctid каждой строки изменился — новые номера страниц и offset’ов).
  5. Удаляет старый файл.
  6. Снимает блокировку.

Результат: таблица занимает столько места, сколько занимают живые кортежи плюс минимальный page overhead. Никакого bloat’а, индексы тоже свежие. Но цена — высока:

  • AccessExclusiveLock на всё время операции. На таблице 100 GiB это часы простоя. SELECT, INSERT, UPDATE — всё ждёт.
  • Двойной дисковый объём в момент операции. Старый файл существует, пока операция не завершилась; новый файл уже занял (примерно) свой финальный размер. Если у вас bloat 50% и таблица 100 GiB, нужно ~150 GiB свободного диска: 100 старых + 50 новых.
  • Перестройка всех индексов. Это часто занимает больше времени, чем сам копирующий проход — особенно если индексов много и они большие.
VACUUM (lazy) vs VACUUM FULL — что меняется

Слева: lazy VACUUM на том же месте, файл сохраняется, free space внутри страниц. Справа: VACUUM FULL переписывает таблицу в новый relfilenode, удаляет старый.

VACUUM (lazy)ShareUpdateExclusiveLock
Файл relfilenodeтот же, размер не меняется
Содержимое страницdead tuples → LP_UNUSED
FSMобновлён, slot'ы доступны
Индексыcleanup, не пересоздаются
Конфликтытолько с DDL и другим VACUUM
Дисковая нагрузкаO(touched pages)
VACUUM FULLAccessExclusiveLock
Файл relfilenodeновый, старый удаляется
Содержимое страницплотная упаковка, нет dyr
FSMпересоздаётся с нуля
ИндексыREINDEX всех индексов
Конфликтысо всем, включая SELECT
Дисковая нагрузкаO(live tuples) + двойной объём временно

Когда VACUUM FULL оправдан

Несколько разумных сценариев:

  • Аномальный bloat после однократного события. Был запущен пакетный DELETE на 80% данных. Таблица «вспухла» до 5x от живого размера и больше так не будет. Стоит один раз сжать.
  • Подготовка к миграции / pg_dump. Перед pg_dump или физическим переездом часто хочется «причесать» БД. На preprod или maintenance-окне VACUUM FULL — нормальный шаг.
  • Маленькие таблицы. На таблице ~1 GiB VACUUM FULL занимает секунды. Если её можно блокировать на пару минут — это самый простой способ.
  • Системные / read-only таблицы. Если таблица обновляется раз в сутки cron’ом, ночное окно — отличный момент.

И сценарии, когда VACUUM FULL категорически не подходит:

  • OLTP-таблица в продакшене 24/7. AccessExclusiveLock остановит весь сервис на часы.
  • Таблица с активной репликацией. VACUUM FULL генерирует много WAL (пишет всю таблицу заново) и может перегрузить replication lag.
  • Таблица с активной FK-связью. Заблокированная таблица может породить deadlock на смежных таблицах, которые ждут lock.

Альтернатива: pg_repack и pg_squeeze

pg_repack (extension от NTT, де-факто стандарт) и более новый pg_squeeze решают ту же задачу — переписать таблицу плотно, вернуть место ОС — но без AccessExclusiveLock. Идея:

  1. Создаётся пустая копия таблицы со всей структурой.
  2. Развешиваются триггеры на исходную таблицу: каждый INSERT/UPDATE/DELETE в течение операции записывается в специальную log-таблицу.
  3. Происходит первичный bulk copy: INSERT INTO new_table SELECT * FROM old_table. Это самая длинная фаза, и она идёт под слабой блокировкой (SELECT/INSERT работают).
  4. Применяются записи из log-таблицы к новой копии (catch-up).
  5. В самом конце — короткая (миллисекунды-секунды) фаза AccessExclusiveLock: переключение имён файлов и DROP старой таблицы.
  6. Пересоздаются индексы. У pg_repack есть онлайн-режим — индекс строится в новой таблице без блокировки старой.

Цена и оговорки:

  • Нужны первичный ключ или уникальный индекс на таблице — без него pg_repack отказывается работать (он использует PK для catch-up).
  • Тот же двойной дисковый объём, что у VACUUM FULL.
  • Лишний WAL-трафик (две полные копии данных).
  • Внешний инструмент: его нужно установить (как extension в Postgres + CLI на сервере) и интегрировать с мониторингом.

В продакшене 24/7 — pg_repack по умолчанию. VACUUM FULL — для maintenance-окон и не-критичных таблиц.

Демо: смотрим, как VACUUM FULL уплотняет таблицу

В pglite VACUUM FULL работает. Сделаем массовый DELETE и сравним размер таблицы до и после VACUUM FULL.

Удалим 80% orders, посмотрим размер до и после обычного VACUUM. Размер не должен сильно измениться:

PostgreSQL

После DELETE 80% строк и регулярного VACUUM таблица занимает почти столько же — потому что VACUUM не возвращает место. Теперь применим VACUUM FULL:

VACUUM FULL после массового DELETE. Размер должен заметно упасть:

PostgreSQL

Размер после VACUUM FULL должен быть ~1/5 от исходного: мы оставили 20% строк, VACUUM FULL переписал таблицу плотно, новый файл содержит только эти 20%.

Скрытая стоимость: индексы

VACUUM FULL пересоздаёт все индексы — это часто доминирующая часть времени операции. На таблице с 5 индексами по 10 GiB каждый VACUUM FULL может занять часы, из которых сама копия heap — десяток минут, а остальное — построение индексов с нуля.

Альтернатива, если нужно сэкономить время и можно потерпеть отдельный простой на индексы: REINDEX TABLE CONCURRENTLY — пересоздаёт индексы по очереди, без блокировки. Но это уже отдельная операция.

И второе: после VACUUM FULL все ctid’ы поменялись. Если ваш код где-то полагается на ctid (это плохая практика, но бывает в админских скриптах) — он сломается. Также все «холодные» индексные записи в WAL/replication slot’ах теперь указывают в никуда — реплика должна догнать VACUUM FULL.

Проверка знанийKnowledge check
У вас OLTP-таблица events в продакшене 24/7, размер 200 GiB, bloat 60% (т.е. 120 GiB живых данных, 80 GiB мусора). Какой инструмент выбрать, чтобы вернуть место ОС, и какие риски нужно учитывать?
ОтветAnswer
Однозначно pg_repack. VACUUM FULL на OLTP 24/7 неприемлем: он возьмёт AccessExclusiveLock на часы — все SELECT/INSERT/UPDATE/DELETE будут ждать. Это полная остановка сервиса. pg_repack делает то же самое (переписывает таблицу плотно, возвращает место ОС), но под слабой блокировкой: триггеры захватывают изменения в log-таблицу, делается bulk copy, потом catch-up, и в самом конце короткая (миллисекунды) фаза AccessExclusiveLock для переименования файлов. Риски: (1) нужен PK или unique index на таблице — pg_repack без него не работает; (2) двойной дисковый объём во время операции — нужно ~120 GiB свободного диска (новая копия) плюс log-таблица; (3) лишний WAL-трафик, может перегрузить реплику — лучше делать в окно меньшей нагрузки; (4) триггеры на таблице конфликтуют с другими триггерами, если они есть — проверить заранее.

Чек-лист

  • VACUUM FULL — переписывает таблицу в новый relfilenode, плотно упаковывает живые кортежи, пересоздаёт все индексы.
  • Берёт AccessExclusiveLock на всё время операции — блокирует даже SELECT. Не подходит для OLTP 24/7.
  • Требует двойной дисковый объём временно — старый файл существует, пока операция не завершилась.
  • pg_repack / pg_squeeze — внешние extension, делают то же самое без эксклюзивной блокировки. Триггеры + log-таблица + catch-up + короткое переименование. Требуют PK/unique index.
  • Когда оправдан VACUUM FULL: после однократного аномального события (массовый DELETE), маленькие таблицы, maintenance-окно.
  • Когда нельзя: OLTP-продакшен 24/7, таблицы с активной репликацией и FK на горячий путь.
  • Все ctid после VACUUM FULL меняются — индексы пересоздаются, slot’ы реплики догоняют новые позиции.
Блокировки строк: SELECT FOR UPDATE, NOWAIT, SKIP LOCKED

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какую блокировку берёт VACUUM FULL и какие операции на таблице она блокирует?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5