VACUUM и раздувание таблиц
MVCC оставляет мёртвые кортежи после каждого UPDATE и DELETE; autovacuum их убирает. Когда он отстаёт, bloat раздувает каждый скан — а exclusive-блокировка VACUUM FULL это неверное лекарство на проде.
Таблица-очередь — строки вставляются, обрабатываются, удаляются, тысячи в минуту — держит 4000 живых строк в любой момент. Её размер на диске 9 ГБ. SELECT count(*), который должен быть мгновенным, занимает 6 секунд, потому что таблица на 99.9% из мёртвых кортежей, мимо которых планировщику всё равно надо просканировать. Кто-то в панике в 2 часа ночи запускает VACUUM FULL и блокирует таблицу на 11 минут посреди инцидента. И раздувания, и лекарства можно было избежать. Этот урок — почему MVCC создаёт мусор и как его собирать без простоя.
Откуда берутся мёртвые кортежи
Почему таблица вырастает до 9 ГБ, держа всего 4000 живых строк? Механизм встроен в каждую запись. Глава MVCC трека databases выводит это глубоко; SQL-автору нужно следствие. При MVCC Postgres никогда не перезаписывает строку на месте. UPDATE пишет новую версию строки и помечает старую мёртвой; DELETE просто помечает строку мёртвой. Старая версия задерживается, потому что снимок какой-то ещё работающей транзакции может захотеть её увидеть. Поэтому каждый UPDATE и DELETE порождает мёртвый кортеж — строку, невидимую новым запросам, но всё ещё физически занимающую страницу на диске.
Пока эти мёртвые кортежи не очищены, они стоят дважды: занимают место, и каждый последовательный или индексный скан всё равно читает страницы, на которых они сидят, и пропускает их. Таблица на 90% из мёртвых кортежей делает примерно 10× I/O за те же живые данные.
-- Посмотри ущерб на любой таблице:
SELECT relname, n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'events', 'line_items')
ORDER BY n_dead_tup DESC;Autovacuum: что его запускает
autovacuum — фоновый процесс, запускающий VACUUM автоматически. Он не ужимает файл — он помечает место мёртвых кортежей как переиспользуемое, чтобы новые строки заполняли дыры вместо расширения таблицы. Он также запускает ANALYZE, держащий статистику свежей (урок 03), и обновляет visibility map.
Он срабатывает на таблицу, когда:
n_dead_tup > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × n_live_tupДефолты: threshold = 50, scale_factor = 0.2. Так таблица автовакуумится, когда мёртвые кортежи превышают 20% живых строк. Эти 0.2 нормальны для таблицы в 10k строк (вакуум при 2k мёртвых), но ужасны для таблицы в 100M строк — он ждёт 20M мёртвых кортежей, к которым сканы уже раздуты. Прод-правило: снижай scale_factor на больших/горячих таблицах, например ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.02);, чтобы он вакуумил при 2% нагрузки.
Visibility map и index-only scans
VACUUM поддерживает visibility map: битмап, помечающий страницы, где каждый кортеж видим всем транзакциям. Именно это делает возможным index-only scan — если страница вся-видима, Postgres может ответить из одного индекса без обращения к heap для проверки видимости (глава индексов трека databases копает глубже). Раздутая, недовакуумленная таблица имеет устаревший visibility map, поэтому «index-only» сканы тихо деградируют в heap-обращения. Вакуум — не просто сборка мусора; это то, что держит твой быстрейший тип скана быстрым.
VACUUM против VACUUM FULL — и прод-ловушка
VACUUM(обычный) — возвращает мёртвое место на месте под переиспользование. Берёт лишьSHARE UPDATE EXCLUSIVEблокировку; чтения и записи продолжаются. Это то, что запускает autovacuum. Он не возвращает диск ОС, но останавливает рост раздувания.VACUUM FULL— переписывает всю таблицу в новый компактный файл, возвращая место ОС. Но он берётACCESS EXCLUSIVEблокировку на всю перезапись — каждое чтение и запись блокируются до конца. На большой таблице это минуты полного простоя. Никогда не запускайVACUUM FULLна живой прод-таблице.
Чтобы реально вернуть диск на раздутой прод-таблице, используй pg_repack (расширение): он перестраивает таблицу в свежую копию онлайн, беря exclusive-блокировку только на короткий финальный своп. Та же компактизация, без простоя.
▸Частая ошибка
Классический инцидент: долго работающая транзакция (зависший BEGIN, застрявший аналитический запрос, забытая psql-сессия) удерживает xmin horizon — старейший снимок, всё ещё в использовании. Autovacuum не может вернуть ни один кортеж новее этого горизонта, на любой таблице, потому что та древняя транзакция может всё ещё захотеть его увидеть. Так одно забытое соединение idle in transaction может раздуть bloat по всей БД, пока autovacuum работает постоянно, но не возвращает ничего. Проверь SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY xact_start; и задай idle_in_transaction_session_timeout, чтобы убивать их автоматически. Поэтому «autovacuum работает, но bloat растёт» почти всегда проблема долгой транзакции, а не настройки autovacuum.
Высоконагруженная таблица-очередь держит 4000 живых строк, но 9 ГБ на диске и медленна в сканировании. Что происходит?
Нужно вернуть диск с сильно раздутой таблицы на живой прод-системе. Лучший инструмент?
Заполни пропуск: обычный VACUUM помечает место мёртвых кортежей как _______ для новых строк, но не ужимает файл; VACUUM FULL переписывает файл, чтобы вернуть диск ОС, ценой exclusive-блокировки.
- 01Почему каждый UPDATE и DELETE создаёт мёртвый кортеж и чего это стоит?
- 02Что запускает autovacuum и почему дефолтный scale_factor — проблема на больших таблицах?
- 03Чем различаются VACUUM, VACUUM FULL и pg_repack и что безопасно на проде?
MVCC никогда не перезаписывает строку, поэтому каждый UPDATE и DELETE оставляет мёртвый кортеж, задерживающийся на диске, пока вакуум его не вернёт — а до тех пор он раздувает таблицу и вынуждает каждый скан читать мимо него, легко 10× I/O на сильно нагруженной таблице. Autovacuum возвращает это место на месте (не ужимая файл), обновляет статистику и поддерживает visibility map, держащий index-only scans быстрыми; он срабатывает при threshold + scale_factor × live_rows, и дефолтный scale factor 0.2 слишком мягок для больших таблиц — снижай его на горячих таблицах. Чтобы вернуть реальный диск, VACUUM не вернёт (он лишь освобождает место под переиспользование), а VACUUM FULL вернёт, но с ACCESS EXCLUSIVE блокировкой, означающей простой — поэтому используй pg_repack для онлайн-компактизации. А когда autovacuum работает, но bloat растёт, причина почти всегда — долгоживущее соединение idle in transaction, прибивающее xmin horizon, так что ничего новее нельзя вернуть по всей БД. Мониторь n_dead_tup и pg_stat_activity; тюнь до того, как это станет ночным инцидентом.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.