Дайджесты новостей
Изометрический архитектурный разрез процесса онлайн-переупаковки таблицы REPACK CONCURRENTLY в PostgreSQL 19 без блокировки транзакций.

Команда REPACK в PostgreSQL 19: встроенная онлайн-очистка таблиц без простоя

Если ты администрируешь базы данных на PostgreSQL, то наверняка не раз сталкивался с феноменом bloat — прогрессирующим раздуванием таблиц и индексов на диске. Вроде бы удалили половину старых записей через DELETE, а размер базы на диске не уменьшился ни на мегабайт. Запросы начинают работать медленнее, кэш shared_buffers вымывается мертвыми страницами, а свободное место в хранилище тает на глазах.

Что делать? Запустить VACUUM FULL? В тестовой среде — пожалуйста. Но в продакшене VACUUM FULL запрашивает жесткую блокировку ACCESS EXCLUSIVE. Она намертво замораживает любые операции чтения и записи к таблице. Если таблица весит пару терабайт, сервис встанет на часы, а бизнес понесет миллионные убытки.

Годами сообщество спасалось сторонними утилитами вроде pg_repack, но их установка требовала прав суперпользователя, триггеров и танцев с бубном при репликации. В готовящемся релизе PostgreSQL 19 разработчики ядра наконец закрыли эту многолетнюю боль, внедрив нативную команду REPACK.

Давай разберем инженерный разбор REPACK в PostgreSQL 19 и посмотрим, как устроен этот механизм изнутри.

Проклятие MVCC: как мертвые строки раздувают таблицы и съедают память

Чтобы понять ценность REPACK, нужно вспомнить, как устроена модель многоверсионности PostgreSQL (MVCC).

Когда ты выполняешь операцию UPDATE, Postgres физически не перезаписывает данные на месте. Вместо этого он помечает старую версию строки как невидимую для будущих транзакций (записывая идентификатор транзакции в поле xmax) и создает совершенно новую строку на текущей или соседней странице данных. При выполнении DELETE строка просто помечается удаленной.

Стандартный фоновый демон autovacuum делает важную работу: он проходит по страницам, очищает указатели на мертвые кортежи (dead tuples) и обновляет карту свободного пространства (Free Space Map — FSM), чтобы новые строки могли занять освободившиеся байты. Однако вернуть физическое место операционной системе через системный вызов truncate он может лишь в одном идеальном случае: если абсолютно пустые страницы образовались в самом хвосте физического файла таблицы.

В реальности живые и мертвые строки хаотично перемешаны по миллионам 8-килобайтных страниц:

  • Таблица из 10 миллионов строк может занимать на диске 200 гигабайт вместо расчетных 40;
  • Последовательное сканирование (Seq Scan) вынуждено поднимать с диска в оперативную память гигабайты мертвого мусора;
  • Деревья B-Tree индексов фрагментируются и теряют пространственную плотность.

Историческая команда CLUSTER могла упорядочить строки по индексу, но, как и VACUUM FULL, требовала исключительной блокировки. База оказывалась в заложниках собственной архитектуры.

Архитектура REPACK CONCURRENTLY: четыре фазы онлайн-перестройки

Ключевое нововведение PostgreSQL 19 — поддержка модификатора CONCURRENTLY для нативной команды REPACK. Синтаксис выглядит предельно лаконично:

-- Онлайн-переупаковка раздутой таблицы заказов
REPACK CONCURRENTLY orders;

-- Переупаковка с одновременной физической сортировкой строк по дате
REPACK CONCURRENTLY orders USING INDEX idx_orders_created_at;

Как Postgres умудряется полностью перестроить терабайтную таблицу, не прерывая поток входящих транзакций? Движок выполняет операцию в четыре строгие фазы:

  1. Фаза 1: Мягкая блокировка и создание целевой кучи: Вместо разрушительного ACCESS EXCLUSIVE процесс запрашивает блокировку уровня ShareUpdateExclusiveLock. Она полностью совместима с параллельными SELECT, INSERT, UPDATE и DELETE. Запрещаются только другие тяжелые DDL-операции (ALTER TABLE). В каталоге создается временный файл новой кучи (heap) с уникальным relfilenode.
  2. Фаза 2: Фоновый перенос данных снимка: Движок считывает все живые кортежи из старой таблицы в соответствии со снимком транзакции и максимально плотно упаковывает их в новый файл без пустых дыр. Если указан параметр USING INDEX, строки сортируются на диске, обеспечивая феноменальную скорость последующих диапазонных выборок (Index Scan).
  3. Фаза 3: Логическое декодирование изменений (Catch-up): Пока шла фаза переноса (которая на больших объемах может длиться десятки минут), реальные пользователи продолжали изменять старую таблицу. Ядро перехватывает эти модификации из журнала предзаписи WAL и последовательно накатывает их на новую таблицу, пока дельта отставания не приблизится к нулю.
  4. Фаза 4: Мгновенный атомарный своп: На считанные миллисекунды запрашивается кратковременная эксклюзивная блокировка. Применяются последние несколько десятков изменений из WAL, в системной таблице pg_class идентификаторы файлов меняются местами, а старый раздутый файл безопасно удаляется с диска с возвратом байтов в файловую систему ОС.

Логическое декодирование вместо триггеров: почему pg_repack больше не нужен

Если ты раньше пользовался утилитой pg_repack, то знаешь ее ахиллесову пяту. Утилита работала через пользовательские триггеры:

  • На целевую таблицу вешался триггер, который на каждый INSERT/UPDATE дублировал строку во вспомогательную таблицу журнала;
  • Это удваивало нагрузку на дисковую подсистему и создавало огромный оверхед по CPU;
  • При внезапном сбое процесса утилиты временные триггеры оставались на таблице, вызывая утечки памяти и блокируя миграции;
  • В облачных управляемых СУБД (AWS RDS, GCP Cloud SQL) установка расширения часто блокировалась политиками безопасности.

В PostgreSQL 19 инженеры ядра пошли по принципиально иному пути: они задействовали штатный механизм логического декодирования WAL.

Тот самый механизм, на котором построена логическая репликация Postgres, теперь читает бинарный журнал предзаписи напрямую из ядра. Никаких пользовательских триггеров, никакой двойной записи и никаких рисков оставить таблицу с поврежденными каталогами. Если операция REPACK CONCURRENTLY прерывается системным сбоем, временный файл просто стирается фоновым сборщиком мусора, а рабочая таблица продолжает функционировать без единой ошибки.

Как мы подробно обсуждали, анализируя встроенный REPACK CONCURRENTLY в PostgreSQL 19, интеграция механизма в ядро превращает обслуживание баз данных из рискованной ночной спецоперации в рутинную фоновую процедуру.

Требования к дисковому пространству и права доступа: как безопасно запускать в проде

Несмотря на всю прелесть режима CONCURRENTLY, у него есть два важных инженерных требования, о которых нельзя забывать при планировании работ:

  1. Запас дискового пространства: Поскольку старая таблица со всеми индексами и новая перестраиваемая копия временно сосуществуют на одном дисковом накопителе, свободное место на томе должно составлять не менее 110–120% от текущего физического размера таблицы. Если диск заполнен на 90%, запускать REPACK CONCURRENTLY категорически нельзя — база упадет по нехватке места.
  2. Выделение оперативной памяти: Скорость сортировки строк и построения индексов в фазе 2 напрямую зависит от параметра maintenance_work_mem. В стандартной конфигурации он часто зажат на уровне 64 МБ. Для терабайтных таблиц перед запуском сессии рекомендуется временно поднять лимит:
-- Выделяем 2 ГБ памяти под сортировку в текущей сессии
SET maintenance_work_mem = '2GB';

-- Запускаем онлайн-очистку с физической сортировкой
REPACK CONCURRENTLY billing_transactions USING INDEX idx_billing_created;

Что касается прав доступа, в PostgreSQL 19 введена гранулярная привилегия MAINTAIN. Больше не нужно раздавать администраторам права всемогущего суперпользователя postgres:

GRANT MAINTAIN ON TABLE billing_transactions TO devops_engineer;

Появление нативного REPACK в ядре — это долгожданная революция в обслуживании баз данных. Наконец-то избавление от bloat перестало требовать ночных дежурств и риска остановки бизнес-процессов.