Почему в PostgreSQL вообще появляется bloat
Под postgres bloat обычно понимают «раздувание» таблиц и индексов: физический размер на диске растёт, а полезных данных (с точки зрения пользователя) не прибавляется. В продакшене это проявляется так:
- растёт расход диска и упираетесь в лимиты/квоты;
- хуже производительность из-за лишних страниц: больше чтения и из диска, и из кеша;
- дольше идут backup/restore и репликация из-за выросшего объёма;
- индексы «рыхлеют», растёт число блоков, которые нужно читать.
Корень проблемы в MVCC: при UPDATE/DELETE PostgreSQL не переписывает строку «на месте», а создаёт новую версию, а старую помечает как невидимую. Эти «мертвые» версии (dead tuples) со временем должны быть вычищены VACUUM. Но обычный VACUUM в большинстве случаев не возвращает место операционной системе: он лишь делает его пригодным для повторного использования внутри того же объекта.
Табличный bloat и индексный bloat — это разные истории
У таблиц bloat появляется из-за накопления dead tuples, а также из-за неудачно выбранного fillfactor при интенсивных обновлениях. У индексов эффект похож (лишние страницы), но механизм иной: даже если dead tuples в таблице уже вычищены, индекс может оставаться большим и фрагментированным.
Поэтому в реальной эксплуатации обычно решают две задачи: уплотнить таблицу и привести в порядок индексы. Дальше начинается выбор: VACUUM FULL, pg_repack и/или отдельные операции REINDEX.
Как понять, что пора лечить bloat
Переходить к «тяжёлой артиллерии» стоит не по ощущению «что-то место жрёт», а по наблюдаемым сигналам:
- таблица/индекс заметно выросли, а бизнес-объём данных примерно тот же;
- после массовых удалений место на диске не возвращается;
- время запросов выросло, а планы и профили показывают чтение большого числа блоков;
- автовакуум не успевает, и
n_dead_tupстабильно высокий.
Быстрая диагностика: где «жир»
Минимальный набор запросов, чтобы увидеть кандидатов на перепаковку и понять, что происходит с вакуумом:
-- Топ таблиц по размеру (таблица + TOAST + индексы)
SELECT
n.nspname AS schema,
c.relname AS table,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid)) AS indexes_toast_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog','information_schema')
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
-- Мертвые строки и активность autovacuum по таблицам
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
Если видите большой разрыв между размером и «ожидаемым» объёмом, а n_dead_tup либо долго не падает, либо таблица пережила массовое удаление — есть смысл планировать reclaim (возврат места).
Важно: bloat не всегда «поломка». Если таблица постоянно обновляется и место активно переиспользуется, большой физический размер может быть нормой. Лечить стоит там, где размер стабильно не используется повторно или индексы реально деградировали.
Если хотите глубже разобраться с мониторингом и лечением bloat, удобно держать под рукой отдельный разбор: практика работы с pg_repack и bloat в PostgreSQL.

VACUUM FULL: что делает и почему его боятся
VACUUM FULL переписывает таблицу в новый физический файл, уплотняя данные. В итоге место действительно возвращается операционной системе, но цена за это — блокировки, длительность и нагрузка.
Ключевые свойства VACUUM FULL
- Блокировки: берёт эксклюзивную блокировку на таблицу и фактически останавливает нормальный DML на время операции.
- Возврат места ОС: да, это один из самых прямых способов уменьшить файл таблицы.
- Индексы: индексы фактически пересоздаются на переписанной таблице, что может занять много времени.
- WAL/репликация: генерирует много WAL; на больших таблицах это легко превращается в заметный lag реплик.
- Требования по диску: нужен запас под переписывание. Планируйте консервативно, как под худший случай.
Когда VACUUM FULL оправдан
На практике VACUUM FULL выбирают, когда есть окно обслуживания и вы готовы к блокировке, либо когда нельзя ставить расширения/использовать внешний инструмент. Типовые кейсы:
- небольшие таблицы, где операция займёт минуты, а не часы;
- разовая «уборка» после массовой чистки данных;
- жёсткие ограничения по регламентам на установку расширений.
pg_repack: тот же эффект, но с минимальными блокировками
pg_repack — инструмент, который позволяет уплотнить таблицу и/или индексы с минимальным временем блокировки. По сути он делает компактную копию объекта и аккуратно переключает её вместо исходной.
Как pg_repack работает на практике
Если очень грубо, логика такая:
- создаёт рядом новую «упакованную» таблицу;
- отслеживает изменения исходной таблицы, чтобы догонять новую копию;
- копирует данные и строит индексы на новой версии;
- на короткое время берёт блокировку и выполняет атомарную подмену.
Благодаря этому сервис обычно продолжает работать, а «окно» блокировки сводится к переключению. Но на горячих таблицах и при длинных транзакциях это окно может растянуться заметно сильнее, чем ожидается.
Плюсы pg_repack
- Минимум простоя: в большинстве случаев это «почти онлайн» операция.
- Возврат места ОС: да, файл реально уменьшается.
- Гибкость: можно идти точечно (таблица/индекс), не трогая весь кластер.
Минусы и риски pg_repack, о которых часто забывают
- Запас по диску: пока создаётся новая копия, нужно место почти как под второй комплект данных плюс индексы.
- Нагрузка: чтение таблицы, построение индексов, активная запись WAL — это I/O и CPU.
- Долгие транзакции: могут мешать финальной подмене и удерживать блокировки дольше.
- DDL-конкуренция: параллельные
ALTER TABLEи миграции схемы — частая причина сбоев; лучше заморозить DDL на время операции. - Операционная дисциплина: нужен контроль репликации, WAL, свободного места и тайминга.
pg_repack vs VACUUM FULL: сравнение по критериям
Выбор лучше делать не по «красивее/страшнее», а по ограничениям продакшена:
- Блокировки:
VACUUM FULLблокирует таблицу надолго;pg_repackобычно блокирует кратко на финальном переключении. - Время выполнения: зависит от объёма и I/O;
pg_repackможет идти дольше из-за механики догонки изменений, но выигрывает отсутствием простоя. - Требования к диску: обоим нужен запас, но у
pg_repackон почти всегда должен быть «комфортным», иначе операция просто не начнётся или сорвётся по месту. - Влияние на репликацию: оба генерируют WAL; на больших таблицах легко получить лаг.
- Предсказуемость:
VACUUM FULLпроще по составу действий; уpg_repackбольше движущихся частей, зато меньше downtime.
Где тут REINDEX и зачем он нужен
Часть «проблем производительности» — это именно индексный bloat или деградация структуры индекса. Важно держать границы инструментов:
REINDEXперестраивает индекс, но не уменьшает таблицу.VACUUM FULLпереписывает таблицу и приводит индексы в порядок в процессе.pg_repackможет уплотнить и таблицу, и индексы с минимальными блокировками.
Когда достаточно REINDEX
Если таблица сама по себе относительно компактная, а проблема явно в индексе (например, индекс «в разы больше нормы» или резко вырос после серии обновлений), иногда достаточно перестроить индекс. Для продакшена обычно рассматривают конкурентное перестроение там, где оно уместно, чтобы снизить влияние на запись.
Сценарий «таблицу не трогаем, индексы лечим»
Этот подход часто выручает, когда:
- таблица активно пишется, и перепаковка слишком рискованна по времени и нагрузке;
- мало диска, и под «вторую копию таблицы» места нет;
- проблема локализована в одном-двух индексах (например, на колонках с интенсивными обновлениями).
Практический чек-лист: как делать перепаковку безопасно
Неважно, выбрали вы VACUUM FULL или pg_repack: подготовка обычно важнее самого инструмента.
1) Проверьте свободное место и временные файлы
Самая частая авария — закончился диск в процессе. Для больших таблиц закладывайте запас под:
- временную копию данных;
- построение индексов;
- рост WAL во время операции;
- временные файлы сортировок при создании индексов.
2) Найдите и уберите «долгоживущие» транзакции
Длинные транзакции мешают и вакууму, и repack: удерживают старые версии строк и могут растянуть блокировки в самый неподходящий момент.
-- Топ долгих транзакций
SELECT
pid,
usename,
state,
xact_start,
now() - xact_start AS xact_age,
wait_event_type,
wait_event,
left(query, 200) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 20;
3) Заморозьте DDL-миграции на время операции
Любые ALTER TABLE, добавление индексов, переименование колонок во время перепаковки — это лишняя неопределённость. На практике проще ввести «freeze» на DDL на время обслуживания.
4) Контролируйте репликацию и WAL
Если есть реплики, заранее определите допустимый lag. На крупных таблицах перепаковка легко «заливает» канал WAL, а следом ухудшает RPO/RTO и задержки чтения с реплики. По теме восстановления и роли WAL полезно: как устроен PITR и восстановление PostgreSQL через WAL.
5) Планируйте окно и откат
Даже если pg_repack «почти онлайн», лучше иметь окно пониженной активности и заранее подготовить:
- актуальный бэкап;
- критерии остановки операции (когда вы её прерываете);
- план возврата, если после перепаковки что-то пошло не так.
Типовые сценарии выбора
Сценарий A: таблица очень большая, сервис 24/7
Чаще всего выбор склоняется к pg_repack. Если bloat связан с жизненным циклом данных (например, вы постоянно удаляете «старое»), иногда правильнее лечить причину: партиционирование и дешевое «выбрасывание» старых партиций вместо массовых DELETE. В инфраструктуре это удобнее вести на предсказуемых ресурсах (например, на VDS с гарантированным диском и I/O).
Сценарий B: есть ночное окно обслуживания и умеренные объёмы
Можно рассматривать VACUUM FULL как более простой и предсказуемый вариант, особенно если таблица не огромная и вы уверены, что уложитесь по времени и по месту.
Сценарий C: место на диске заканчивается прямо сейчас
«Заканчивается диск» — плохой момент для перепаковки: и VACUUM FULL, и pg_repack требуют свободного места. Часто сначала приходится сделать одно из следующих действий:
- убрать старые бэкапы и временные файлы;
- расширить том/диск;
- перенести тяжёлые объекты в отдельный tablespace, если архитектура это допускает.

Что сделать, чтобы bloat возвращался реже
Перепаковка лечит симптом. Чтобы реже к ней возвращаться, обычно помогают:
- тонкая настройка autovacuum под вашу нагрузку (пороги, частота, cost limit/delay);
- корректный
fillfactorдля таблиц и индексов с частыми обновлениями; - партиционирование по времени/ключу, чтобы «выкидывать» старые данные без массовых
DELETE; - регулярный мониторинг роста объектов и доли dead tuples;
- периодический
REINDEXдля реально деградирующих индексов.
Если вы часто делаете массовые удаления в больших таблицах, почти всегда стоит подумать о партициях. Сброс партиции возвращает место намного дешевле по ресурсам, чем переписывание гигантской таблицы.
Минимальные команды: что запускать
Чтобы не путаться в терминах:
VACUUM— чистит мёртвые версии, место ОС обычно не возвращает.VACUUM FULL— переписывает таблицу и возвращает место ОС, но блокирует.REINDEX— перестраивает индексы (и может вернуть место, занятое индексом), но таблицу не уменьшает.pg_repack— уплотняет таблицу/индексы почти онлайн, но требует диска и даёт нагрузку.
-- VACUUM FULL (опасно для продакшена без окна обслуживания)
VACUUM FULL VERBOSE public.big_table;
-- REINDEX конкретного индекса
REINDEX INDEX public.big_table_some_idx;
-- REINDEX таблицы (все её индексы)
REINDEX TABLE public.big_table;
Запуск pg_repack зависит от того, как он установлен и как устроен доступ к вашей базе, но общая идея всегда одна: выбрать объект, убедиться в наличии места, запустить операцию в подходящее окно и сравнить размер до/после.
Итог: как выбрать между pg_repack и VACUUM FULL
- Нужен гарантированный возврат места и есть окно простоя — выбирайте
VACUUM FULL(проще и предсказуемее). - Простоя быть не должно, таблица большая и активная — чаще выбирают
pg_repack, но готовятся к нагрузке и требованиям по диску. - Проблема локально в индексах — начните с
REINDEX, не трогая таблицу целиком.
В реальном продакшене «правильный» вариант почти всегда определяется тремя ограничениями: сколько у вас свободного диска, насколько критичны блокировки и какую нагрузку на I/O и WAL вы можете себе позволить.


