Когда вы в последний раз очищали БД от старых записей? А ведь раздувание таблиц и индексов в PostgreSQL из-за неактуальных данных — один из часто недооцениваемых источников «тихих» деградаций. Запросы потихоньку становятся медленнее, бэкапы — тяжелее, а место на диске расходуется неэффективно. В итоге любое лишнее уведомление от алерта или доля секунды задержки могут обернуться сбоем системы.

Привет, Хабр! На связи Александр Гришин. Я руковожу развитием облачных баз данных и S3-хранилища в Selectel. В этой статье предлагаю разобраться с одной из тех проблем, которые редко попадают в мониторинг, но легко становятся причиной инцидентов в проде. Посмотрим, чем pg_repack отличается от VACUUM FULL, какие особенности есть у каждого подхода и как использовать repack без дополнительных телодвижений. Статья будет полезна инженерам, поддерживающим PostgreSQL в продакшене, разработчикам облачных приложений и SaaS-сервисов и просто любопытным, кто стремится лучше понять, что происходит под капотом PostgreSQL в разных ситуациях. Погнали!

Используйте навигацию, если не хотите читать текст целиком:
Откуда берется bloat
Что дает стандартный VACUUM
Как работает pg_repack
pg_repack в DBaaS Selectel
Рассмотрим расширение детальнее
Ограничения и грабли
Итоги

Откуда берется bloat


Bloat (раздувание) — это состояние, когда таблица или индекс занимает на диске значительно больше места, чем реально нужно. Причиной может быть механизм MVCC (многоверсионность), используемый PostgreSQL для обеспечения согласованности транзакций и параллелизма. Вот как это работает.

При выполнении запросов UPDATE или DELETE, старые версии строк помечаются «мертвыми», но физически остаются в файле на диске:
  • UPDATE — на диске остается старая строка и появляется новая;
  • DELETE — на диске остается старая строка, помеченная как dead.

Получается, что чем чаще меняются данные, тем больше пустого места образуется внутри страниц таблицы и индексов. Таблица раздувается, файл на диске растет, падает cache hit ratio, растут I/O, а планировщик отрабатывает менее оптимально. Подробнее эту проблему я уже разбирал в статье об оптимизации PostgreSQL. И как обещал, раскрываю тему дальше — посмотрим, как можно с этим бороться.

Для начала предлагаю вам посмотреть на физический размер вашей таблицы. Например, вот таким образом:

-- Размер таблицы на диске (в байтах)
SELECT pg_relation_size('your_table');
-- В человекочитаемом формате
SELECT pg_size_pretty(pg_relation_size('your_table'));



Что дает стандартный VACUUM


VACUUM — это команда в PostgreSQL, которая используется для очистки базы данных от «мертвых» (неактуальных) строк и освобождения занимаемого ими места. VACUUM очищает их, чтобы вернуть пространство обратно системе и предотвратить раздувание таблиц. А еще обновляет статистику, важную для EXPLAIN — встроенного оптимизатора запросов.

Есть несколько видов команды.
  • VACUUM — просто помечает устаревшие строки как доступные для повторного использования (как reusable). Не уменьшает физический размер файлов таблицы.
  • VACUUM FULL — выполняет более глубокую очистку, уплотняет таблицу и возвращает свободное место обратно операционной системе, уменьшая физический размер файла. Этот процесс требует блокировки таблицы, поэтому выполняется дольше и блокирует другие операции.
  • AUTOVACUUM — автоматический процесс в PostgreSQL, который запускается в фоне и периодически выполняет VACUUM для поддержания здоровья базы.

VACUUM FULL решает проблему полностью, но эксклюзивно блокирует таблицу. Для продакшн‑нагрузки это почти всегда неприемлемо.

Пример


Допустим, есть таблица на 1 ГБ и вы удаляете 70% строк. Рассмотрим, как это работает.
  • Выполняем VACUUM. Таблица все еще весит 1 ГБ на уровне файловой системы в ОС, но ~700 МБ может быть повторно использовано PostgreSQL.
  • После VACUUM FULL таблица сжимается, допустим, до 300 МБ, т. к. PostgreSQL копирует только живые строки в новый файл, а затем подменяет им старый, освобождая место на уровне ОС.

Простая команда:

VACUUM;

  • Очищает все таблицы в текущей базе от «мертвых» строк.
  • Не блокирует операции SELECT, INSERT, UPDATE, DELETE.
  • Не уменьшает физический размер файлов (файлы таблиц остаются того же размера).
  • Освобождает место для повторного использования при вставках.

Можно запустить VACUUM для конкретной таблицы:

VACUUM public.orders;

  • Очищает только таблицу orders в схеме public.

Добавим обновление статистики для планировщика запросов:

VACUUM ANALYZE public.orders;

  • Одновременно очищает таблицу и собирает статистику (анализирует распределение данных).
  • Статистика используется планировщиком запросов для оптимизации выполнения запросов.

Полностью перепишим таблицу в новый файл:

VACUUM FULL products;

  • Делает полную очистку таблицы products и сжимает ее, уменьшая размер на диске.
  • Физически перемещает строки и освобождает место, возвращая его операционной системе.
  • Требует эксклюзивной блокировки таблицы — другие операции с таблицей будут ждать завершения.
  • Используется, если таблица сильно раздулась (например, после массового удаления).

Дополнительно для лучшего понимания механики работы можно использовать параметр VERBOSE. Он выводит подробную информацию о процессе вакуума.

VACUUM VERBOSE public.orders;

Пример вывода:


Простая аналогия


Эта механика станет понятнее, если представить таблицу в PostgreSQL как обычный рабочий блокнот.
  • Файл таблицы на уровне ОС — это бумажный блокнот.
  • Каждая строка — это строка в блокноте.
  • Когда мы удаляем строку (через DELETE), PostgreSQL просто зачеркивает ее, но не вырывает лист.
  • VACUUM смотрит на зачеркнутые строки и помечает их как «теперь доступные». В будущем PostgreSQL сможет снова записать туда что-то.
  • Но блокнот сам по себе не становится тоньше — его физический объем не изменился.

Теперь рассмотрим механику работы VACUUM FULL в этой аналогии.
  • Таблица — это все тот же блокнот, в котором мы постоянно что-то записываем, зачеркиваем и исправляем.
  • Со временем в блокноте становится много зачеркнутых строк, и он выглядит захламленном.
  • PostgreSQL мог бы продолжать писать между зачеркнутыми строками (что и делает обычный VACUUM), но кажется, что место используется неэффективно.
  • Мы можем взять новый чистый блокнот.
  • Переписать в него только актуальные, нужные строки.
  • После этого заменить старый захламленный блокнот на новый.
  • На все время работы мы блокируем старый блокнот для новых изменений.

Как работает pg_repack


pg_repack — это расширение для PostgreSQL, которое удаляет мертвые строки, оставшиеся после DELETE и UPDATE. Это позволяет дефрагментировать и компактно переписать таблицу или индекс без блокировки таблицы, в отличие от VACUUM FULL.
  1. Создает временную «чистую» таблицу‑копию и индексы.
  2. Копирует в нее все актуальные данные.
  3. Следит за всеми изменениями в оригинальной таблице .
  4. Догоняет изменения, произошедшие в исходной таблице.
  5. Переключает имена таблиц за доли секунды.
  6. Удаляет старый bloat‑файл.

Блокировка все еще нужна, но только на пятом шаге и длится миллисекунды.

Сравнение


Механизм Освобождает место Уменьшает размер файла Требует блокировку
VACUUM Да Нет Нет
VACUUM FULL Да Да Да
pg_repack Да Да Только на финальной фазе переключения таблиц


Аналогия для pg_repack


Продолжим представлять таблицу PostgreSQL как рабочий блокнот, в котором мы много пишем, зачеркиваем, иногда полностью переписываем всю информацию в новый.
  • Мы понимаем, что блокнот раздулся от зачеркнутых строк, и его пора менять.
  • Раньше мы останавливали все, чтобы сесть и вручную перенести актуальные данные в новый блокнот (VACUUM FULL). Но сегодня нам нельзя прерывать работу — кто-то все еще читает и пишет в наш блокнот!
  • Тогда мы заводим новый блокнот рядом и говорим pg_repack: «Садись рядом со мной и переписывай актуальные данные в чистовую копию. Но учти, я буду продолжать работать со старым блокнотом, а тебе нужно следить за изменениями, которые я буду вносить, и тоже добавлять и удалять их в этом новом блокноте. Когда догонишь меня, мы переключим нагрузку на новый блокнот».

pg_repack начинает:
  1. копировать живые строки из старого блокнота в новый;
  2. следить за всеми новыми записями;
  3. параллельно добавлять их в новый блокнот тоже;
  4. когда все готово, быстро меняет местами старый и новый блокнот с минимальной блокировкой (обычно несколько сотен миллисекунд).

pg_repack в DBaaS Selectel


В сервисе баз данных Selectel расширение ставится кликом в панели, после чего функции pg_repack становятся доступны из PostgreSQL. С полным списком поддерживаемых расширений можно ознакомиться в документации.

1. Разверните кластер в панели управления.


2. Создайте пользователя.


3. Создайте базу данных.


4. Добавьте расширение


5. Подключитесь и используйте готовую облачную базу данных.


Рассмотрим расширение детальнее


Шаг 1. Подготовим тестовую таблицу и искусственно раздуем ее:

-- Создаем таблицу с 1 млн строк, каждая с payload ~100 байт
CREATE TABLE bloated AS
SELECT id, repeat('x', 100) AS payload
FROM generate_series(1, 1000000) AS id;

Для этого обновим 50% строк, чтобы создать «мертвые» версии старых данных:

UPDATE bloated
SET payload = repeat('y', 100)
WHERE id % 2 = 0;

Шаг 2. Измерим размер до репака:

SELECT pg_size_pretty(pg_total_relation_size('bloated')) AS size_before;
-- Результат: ~200 MB

Шаг 3. В управляемой базе данных Selectel DBaaS его нужно запускать с клиентской машины, подключаясь по внешнему адресу::

pg_repack -k -h <host> -p 6432 \
          -U <user> \
          -d <database>;

После запуска утилита автоматически создаёт копию таблицы, переносит данные без «мусора» и атомарно подменяет оригинальную таблицу. DML-операции при этом ставятся на короткую паузу в конце процесса (на этапе переключения).

Шаг 4. Проверим размер после:

SELECT pg_size_pretty(pg_total_relation_size('bloated')) AS size_after;
-- Новый результат: ~110 MB

Сравнение


Метод
Размер «до»
Размер «после»
Время выполнения
Доступ к таблице
VACUUM
200 MB
200 MB
3 s
доступна
VACUUM FULL
200 MB
100 MB
15 s
заблокирована
pg_repack
200 MB
110 MB*
8 s
доступна (pause ≤ 200 мс)

Почему размер новой таблицы в результате работы pg_repack может быть больше? Дело в том, что во время работы pg_repack в оригинальную таблицу могли приходить новые транзакции (INSERT/UPDATE/DELETE), и они тоже переносятся в новую таблицу.

Можно исполтзовать пробный запуск (dry run)
Для оценки того, что будет перепаковано:
pg_repack --dry-run ...


Будет выведен список объектов, которые будут обработаны.

Ограничения и грабли


Каждый новый инструмент имеет свои особенности и ограничения, которые полезно принимать во внимание:
  • Учитывая описанную выше механику работы, нужен запас диска, примерно равный или больший, чем репакуемая таблица.
  • Таблица должна иметь первичный ключ или уникальный индекс для корректной идентификации строк.
  • Может дать сильная I/O‑нагрузка: лучше запускать вне пикового окна. И обязательно использовать быстрые диски. Подробнее уже рассказывал в статье о том, что нужно PostgreSQL.
  • Работает на версиях PostgreSQL после 9.1.
  • Для крупных таблиц (сотни гигабайт и выше) репак займет часы — стоит учитывать это при планировании и запускать его в вечернее или ночное окно.

Магии не бывает. Фактически утилита просто переписывает данные из одной таблицы в другую. Это, с одной стороны, позволяет вам обслуживать систему без даунтайма. А с другой, займет больше ресурсов.

Стоит иметь в виду, что интенсивная нагрузка на изменения оригинальной таблицы во время репака сведет на нет всю пользу от данной процедуры. Поэтому в некоторых случаях вам все равно не обойтись без VACUUM FULL. Всегда держите в плане эксплуатации регламентированные окна для обслуживания вашей системы.

Итоги


  • Bloat в PostgreSQL — это не баг, а фича. Побочный эффект MVCC: старые версии строк не удаляются сразу, а копятся в таблице.
  • VACUUM размечает старые строки как переиспользуемые, но не освобождает физически место на диске.
  • VACUUM FULL удаляет bloat, но блокирует таблицу на все время операции.
  • pg_repack убирает bloat почти без простоя и может подойти для обслуживания при невысокой нагрузке на СУБД.
  • Автоматизация возможна: SQL-скрипт с repack.repack_table() можно запускать из внешнего планировщика по расписанию.

Если вы замечаете, что запросы стали выполняться медленнее, а размер базы данных растет быстрее, чем ожидалось, pg_repack может дать быстрый и безопасный прирост производительности, особенно в среде с высокими SLA и ограниченным доступом к серверу.

Попробуйте использовать pg_repack в составе облачного PostgreSQL от Selectel — установка в один клик, запуск прямо из SQL и никакой возни с настройкой расширений на сервере.

А еще мы в Selectel недавно выпустили ультимативный по производительности сервис — первый в России DBaaS на выделенных серверах. Подробнее об этой услуге я уже рассказывал в другой статье.

Обязательно делитесь вашим мнением и опытом в комментариях. В обозримом будущем я продолжу эту тему и расскажу о других полезных расширениях для PostgreSQL.

Комментарии (5)


  1. Melkij
    26.06.2025 08:36

    pg_repack maintainer и автор патчей для поддержки всех версий postgresql начиная с 11.

    Следит за всеми изменениями в оригинальной таблице (использует триггеры и лог WAL).

    WAL мы не используем для отслеживания изменений

    Создает временную «чистую» таблицу‑копию и индексы.
    Копирует в нее все актуальные данные.

    корректности ради: сначала создаём таблицу, потом пишем в неё копию данных из таблицы-источника, только потом создаём новые индексы

    Блокировка все еще нужна, но только на пятом шаге и длится миллисекунды.

    блокировку мы вынуждены брать дважды: в самом начале, т.к. create trigger требует аналогично блокировку, и в конце для подмены relfilenode на новые.
    Миллисекунды блокировки только если никто не мешает (к таблице только быстрые запросы и их можно раздвинуть безболезненно), а вот если к табличке ещё постоянным фоном транзакции по несколько минут - остановите эту активность на время репака либо не ставьте опцию no-kill-backend чтобы репак прибил те транзакции которые мешают взять блокировку.

    SELECT repack.repack_table('bloated');

    это некорректный способ запуска pg_repack. Если это локальный патч вашего форка - упомяните это, пожалуйста, явно.

    по vacuum (не full) - почему в табличке отмечено "Даунтайм для приложения" "да"?

    ну и есть у вакуума одна особенность: он действительно может уменьшить таблицу. Но только хвост датафайлов. То есть от конца таблицы до самого дальнего тапла который не был удалён и вычищен. И зная это, если ещё вариант сжатия таблиц: https://github.com/dataegret/pgcompacttable/blob/master/bin/pgcompacttable
    переместить оставшиеся живые таплы из конца таблицы, затем обычный vacuum действительно вернёт место ОС. Это особо полезно, когда нет троекратного запаса по месту на дисках (на новую копию данных, индексы, и всё это записать в WAL)


    1. GrishinAlex Автор
      26.06.2025 08:36

      Спасибо за столь развернутый и подробный комментарий. Позволю себе отредактироваить и отразить подсвеченные вами моменты в статье.


  1. Sleuthhound
    26.06.2025 08:36

    Забыли сказать самое главное про pg_repack - он требует место на диске, причем как правило x3 (место для новой таблицы равное исходной + переливка данных и потом накат изменений нагенерируют кучу WAL'оф).

    Для примера я репачил таблицу размером 150GB, на диске было 50GB свободного места, в итоге в облачное DBaaS пришлось увеличить диск на 350GB и это прям впритык. А в оканцове таблица стала 140GB и что мы имеем? Мы стали больше платить за диск, тк ресайз вниз у многих облачных провайдеров не поддерживается.

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


    1. GrishinAlex Автор
      26.06.2025 08:36

      Спасибо большое за ваш комментарий, но я явно отразил этот момент с диском в разделе "Ограничения и Грабли".
      Ну и вы абсолютно правы - магии не бывает. Разные инстурменты работают с разными нюансами и ограничениями. Нужно правильно выбирать их под свои задачи.


      1. Sleuthhound
        26.06.2025 08:36

        Простите, я что-то не заметил. Ну немного цифр в коментах не повредит)