Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY

Рейтинг: 67.6% · 8 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY (вы здесь)
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 24. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY

Со временем таблицы и индексы в postgresql начинают занимать на диске больше места, чем реально нужно для хранимых данных. Это явление называют раздуванием, или bloat. Из-за него запросы читают лишние страницы, индексы перестают помещаться в кеш, а место на диске тает без видимой причины. В этом уроке разберем, откуда берется bloat, как его измерить через pgstattuple, и как привести таблицы и индексы в порядок без остановки рабочей базы - с помощью REINDEX CONCURRENTLY и CREATE INDEX CONCURRENTLY.

Изображение

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

Главная причина bloat - механика версионирования строк (MVCC). При UPDATE или DELETE postgresql не стирает старую версию строки сразу. Он помечает ее как устаревшую, но физически она остается в файле, пока на нее ссылается хоть одна активная транзакция. Очистку мертвых версий выполняет vacuum: он освобождает место внутри страниц, чтобы его переиспользовали новые строки. Но vacuum, как правило, не возвращает место операционной системе - таблица просто перестает расти, имея внутри пустоты.

Если таблицу интенсивно обновляют, а autovacuum не успевает или настроен слабо, пустот накапливается много. Файл таблицы становится разреженным: полезных данных мало, а страниц много. То же самое с индексами. B-tree индекс хранит ссылки на версии строк, и при частых обновлениях его страницы заполняются неравномерно, остаются полупустыми после удалений и расщеплений. Раздутый индекс - частая причина того, что один и тот же запрос со временем замедляется, хотя данных будто бы столько же.

Лечение делится на два уровня. Легкий случай - обычный VACUUM, который держит bloat в узде, переиспользуя дыры. Тяжелый случай, когда дыр накопилось критически много, требует физической перезаписи. Для таблиц это VACUUM FULL или pg_repack, для индексов - REINDEX. И VACUUM FULL, и обычный REINDEX берут тяжелую блокировку и останавливают работу с объектом. Поэтому для боевой базы, которая не может позволить себе простой, есть варианты с CONCURRENTLY: они строят новую копию объекта рядом, а блокировку берут лишь на доли секунды в самом конце.

SQL и примеры

Сначала измерим раздувание. Размер объекта смотрим встроенными функциями, а точную долю мусора - расширением pgstattuple.

Код: Выделить всё

CREATE EXTENSION IF NOT EXISTS pgstattuple;

-- Размер таблицы перелетов с индексами и без
SELECT pg_size_pretty(pg_total_relation_size('bookings.flights')) AS total,
       pg_size_pretty(pg_relation_size('bookings.flights'))       AS heap_only;

-- Доля мертвого пространства в таблице
SELECT table_len, tuple_count, dead_tuple_count,
       round(dead_tuple_percent::numeric, 2) AS dead_pct,
       round(free_percent::numeric, 2)       AS free_pct
FROM pgstattuple('bookings.ticket_flights');
Поле dead_tuple_percent показывает мертвые версии, которые еще не вычистил vacuum, а free_percent - свободное место внутри страниц. Если free_percent большое и стабильное - это и есть bloat.

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

Код: Выделить всё

-- Оценим раздувание индекса по номеру билета
SELECT round(avg_leaf_density::numeric, 1) AS leaf_density_pct,
       leaf_pages, leaf_fragmentation
FROM pgstatindex('bookings.tickets_pkey');
Здоровый свежий B-tree дает avg_leaf_density около 90 процентов. Если плотность упала до 50-60, индекс полупустой и его стоит перестроить.

Теперь сама перестройка. Базовый вариант блокирует таблицу на запись (а REINDEX TABLE - и на чтение нужного индекса):

Код: Выделить всё

REINDEX INDEX bookings.tickets_pkey;       -- один индекс
REINDEX TABLE bookings.bookings;           -- все индексы таблицы
Боевой вариант без простоя - с ключевым словом CONCURRENTLY. Postgres строит новый индекс параллельно, не мешая запросам, и в конце атомарно подменяет старый:

Код: Выделить всё

REINDEX INDEX CONCURRENTLY bookings.tickets_pkey;
REINDEX TABLE CONCURRENTLY bookings.ticket_flights;
Добавить совершенно новый индекс на живой таблице тоже можно без блокировки записи:

Код: Выделить всё

CREATE INDEX CONCURRENTLY idx_tf_fare
    ON bookings.ticket_flights (fare_conditions);
Полезная команда - найти индексы, которые вообще не используются и зря раздуваются и обслуживаются:

Код: Выделить всё

SELECT schemaname, relname AS table, indexrelname AS index,
       idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
В PostgreSQL 16 в это представление добавили колонку last_idx_scan - время последнего обращения к индексу, что делает поиск кандидатов на удаление гораздо честнее.

Частые грабли
  • VACUUM FULL и обычный REINDEX берут ACCESS EXCLUSIVE LOCK - это полная остановка работы с таблицей. На боевой базе их запускают только в окно обслуживания.
  • CREATE INDEX CONCURRENTLY и REINDEX CONCURRENTLY нельзя выполнять внутри блока транзакции (BEGIN...COMMIT) - команда упадет с ошибкой.
  • Если CONCURRENTLY прервался, остается невалидный индекс (поле indisvalid = false). Запросы его не используют, но место он занимает. Найдите такой через pg_index и удалите вручную: DROP INDEX CONCURRENTLY.
  • REINDEX CONCURRENTLY временно требует места под две копии индекса. На забитом под завязку диске он не отработает.
  • Долгая транзакция или зависший replication slot держат xmin и не дают vacuum чистить мертвые строки. Сначала разберитесь с долгими транзакциями, иначе bloat вернется сразу после REINDEX.
  • pgstattuple на огромной таблице читает ее целиком и создает нагрузку. Для индексов берите более легкий pgstatindex, а для приблизительной оценки - pgstattuple_approx.
  • Перестройка индекса не лечит раздувание самой таблицы (heap). Для heap нужен VACUUM FULL или pg_repack, REINDEX тут не поможет.
Мини-лаба

Цель - своими глазами увидеть bloat и убрать его без блокировки.
  • Создайте тестовую таблицу: CREATE TABLE t AS SELECT g AS id, md5(g::text) AS v FROM generate_series(1, 500000) g; и индекс CREATE INDEX t_id_idx ON t(id);
  • Посмотрите исходный размер: SELECT pg_size_pretty(pg_total_relation_size('t')); и плотность индекса через pgstatindex('t_id_idx').
  • Сгенерируйте раздувание: UPDATE t SET v = md5(random()::text); выполните этот UPDATE 3-4 раза подряд.
  • Снова замерьте размер и pgstatindex - плотность листьев должна заметно упасть, а размер вырасти.
  • Перестройте индекс без блокировки: REINDEX INDEX CONCURRENTLY t_id_idx; и сравните плотность - она вернется к 90 процентам.
  • Запустите VACUUM (VERBOSE) t; и прочитайте, сколько мертвых строк было удалено.
  • Для сравнения вычистите heap через VACUUM FULL t; и посмотрите, как упал общий размер таблицы.
Контрольные вопросы
  • Почему обычный VACUUM обычно не уменьшает размер файла таблицы на диске?
  • Чем REINDEX CONCURRENTLY отличается от REINDEX по характеру блокировок?
  • Какое расширение и какая функция дадут точную долю мертвого пространства в таблице?
  • Что произойдет, если выполнение CREATE INDEX CONCURRENTLY прервется на середине, и как это исправить?
  • Почему долгая открытая транзакция мешает борьбе с bloat?
  • В каком случае REINDEX бесполезен и нужен VACUUM FULL или pg_repack?
👍2 ❤️3 🔥 😄 🤔
Аватара пользователя
pasha7
Сообщения: 1
Зарегистрирован: 29 май 2026, 16:40

Re: Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY

Сообщение pasha7 »

А можно как-то заранее ловить раздутые индексы, чтобы не ждать пока запросы начнут тормозить? Хочется в мониторинг повесить порог по leaf_density.
👍 ❤️ 🔥 😄 🤔1
Аватара пользователя
jtuac3my
Сообщения: 1
Зарегистрирован: 30 май 2026, 22:56

Re: Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY

Сообщение jtuac3my »

Запустил REINDEX INDEX CONCURRENTLY внутри BEGIN по привычке - получил ошибку. Оказывается, его реально только автокоммитом, без транзакционного блока. Записал, чтоб не наступать второй раз.
👍1 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
Продвинутые индексы: частичные, по выражению, покрывающие
Следующая глава →
Полнотекстовый поиск

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: частичный и покрывающий индекс в postgresql когда нуженjsonb в postgresql операторы и индексацияпартиционирование таблиц postgresql по датематериализованное представление postgresql как обновлятьautovacuum не успевает как настроить postgresqlpostgresql какой индекс выбрать btree gin gist brin

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя