10. VACUUM, ANALYZE

В лекции про многоверсионность 3. MVCC в PostgreSQL мы столкнулись с фундаментальным архитектурным фактом: движок PostgreSQL никогда не модифицирует существующие данные «на месте» (in-place) и физически не стирает строки при вызове DELETE. Каждая операция обновления или удаления оставляет за собой «мертвый» кортеж (dead tuple), помеченный системным идентификатором транзакции t_xmax.

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

За поддержание физической чистоты страниц и математической зоркости планировщика отвечают две фундаментальные системные операции: VACUUM и ANALYZE. Для Go-разработчика понимание их внутренней механики — это граница, отделяющая стабильно работающий бэкенд от внезапного паралича продакшена посреди ночи.


1. VACUUM: Мусорщик в мире MVCC

VACUUM — это процесс, который сканирует таблицы и помечает место, занятое мертвыми строками, как свободное.

Важно понимать: обычный VACUUM не возвращает место операционной системе. Он просто говорит PostgreSQL: "Эти байты внутри 8-килобайтной страницы больше не нужны, сюда можно записать новый INSERT".

flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph PageBefore ["1. Страница Heap до очистки (8 KB)"]
        direction TB
        T1["Живой кортеж A (t_xmax = 0)"]
        T2["Мертвый кортеж B (t_xmax = 100, COMMIT в прошлом)"]
        T3["Живой кортеж C (t_xmax = 0)"]
    end

    subgraph VacuumAction ["2. Проход VACUUM"]
        VProc["VACUUM Worker: <br> • Удаление ссылок из B-Tree индексов <br> • Пометка слота B как свободного <br> • Обновление Free Space Map (FSM) <br> • Выставление бита all_visible в Visibility Map"]
    end

    subgraph PageAfter ["3. Страница после VACUUM (8 KB)"]
        direction TB
        CT1["Живой кортеж A"]:::process
        CFS["[ Свободное окно для записи (Free Space) ]"]
        CT3["Живой кортеж C"]:::process
    end

    PageBefore --> VacuumAction
    VacuumAction --> PageAfter
    CFS -.->|"Новый INSERT из Go"| NewInsert["Новый кортеж D без расширения файла!"]
  

Как физически работает конвейер VACUUM (пошагово)

  1. Сканирование Heap: Воркер вакуума считывает страницы таблицы из shared_buffers или поднимает их с диска.
  2. Проверка горизонта транзакций: Сверяясь с глобальным снимком активных транзакций, процесс выявляет мертвые кортежи, чей t_xmax гарантированно завершен и невидим ни для одной текущей или потенциальной сессии.
  3. Двухфазная очистка индексов: Это критически важный шаг! Сначала вакуум собирает список адресов CTID мертвых строк в буфер maintenance_work_mem, обходит все до единого индексы таблицы (B-Tree, GIN, GiST) и вычищает из их листовых страниц указатели на эти мертвые кортежи.
  4. Очистка страниц Heap: Только после того, как все ссылки из индексов гарантированно удалены, вакуум возвращается к страницам Heap и переводит слоты мертвых кортежей в статус свободного пространства.
  5. Обновление Free Space Map (FSM): В специальный вспомогательный файл (_fsm) записывается точный объем свободного места на каждой странице, чтобы последующие операции INSERT мгновенно находили свободные блоки без линейного сканирования файлов.

VACUUM FULL: Когда всё очень плохо

Существует специальная агрессивная форма очистки — команда VACUUM FULL. В отличие от обычного вакуума, она физически возвращает неиспользуемые байты операционной системе:

  1. Создает абсолютно новый файл таблицы на диске.
  2. Последовательно переносит в новый файл исключительно живые кортежи, упаковывая их максимально плотно без малейших зазоров.
  3. С нуля перестраивает все индексы таблицы.
  4. Физически удаляет старый раздутый файл, возвращая гигабайты пространства ОС.

2. Autovacuum: Твой невидимый помощник

В современных версиях PostgreSQL администраторам редко приходится инициировать очистку вручную: за чистотой следит системный демон — Autovacuum Launcher и пул его дочерних воркеров.

Демон пробуждается раз в период времени, заданный параметром autovacuum_naptime (по умолчанию раз в 1 минуту), опрашивает статистику подсистемы сбора метрик и вычисляет, не пора ли запустить вакуум для конкретной таблицы.

Mechanical Sympathy для Highload

Представьте таблицу на 100 миллионов строк. При стандартном коэффициенте scale_factor = 0.2 Autovacuum запустится только тогда, когда в таблице накопится:
$$50 + (0.2 \times 100\,000\,000) \approx 20\,000\,000 \text{ мертвых строк!}$$
Накопление 20 миллионов мертвых строк означает чудовищное раздувание таблицы (Table Bloat), деградацию дискового ввода-вывода и засорение shared_buffers.
В высоконагруженных проектах для тяжелых таблиц значение scale_factor принудительно снижают до 0.01 (1%) или даже 0.005 (0.5%), обеспечивая непрерывную, мягкую и незаметную для пользователей очистку.


3. ANALYZE: Глаза и математический компас планировщика

Если задача VACUUM — физическое освобождение дискового пространства внутри страниц, то миссия команды ANALYZE — обеспечение интеллектуальной работы планировщика запросов.

Как мы разбирали в 9. Оптимизация PostgreSQL, стоимостной оптимизатор принимает решение о выборе алгоритма (Index Scan, Bitmap Scan, Seq Scan, Hash Join, Merge Join) исключительно на базе математической статистики.
Процесс ANALYZE инспектирует репрезентативную случайную выборку строк таблицы и рассчитывает:

  • Общее число физических страниц и строк в таблице.
  • Selection Cardinality (степень уникальности значений в каждом столбце — n_distinct).
  • Список наиболее частых значений (Most Common Values, MCV) и их процентную частоту.
  • Гистограммы квантилей распределения значений (Histogram Bounds).
flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    Table["Таблица orders <br> (10 млн строк)"]:::process --> Sampler["ANALYZE: <br> Случайная выборка ~30 000 строк"]
    Sampler --> StatsCalc["Расчет метрик: <br> • n_distinct <br> • Гистограммы MCV"]
    StatsCalc --> Catalog["Системный каталог pg_statistic <br> (Используется Planner при каждом EXPLAIN)"]
  

Почему актуальность статистики критична для Go-приложения?

Представьте таблицу задач tasks со столбцом status:

  • 99% строк имеют статус 'COMPLETED'.
  • 1% строк имеют статус 'IN_PROGRESS'.

Если статистика актуальна, то при выполнении запроса SELECT * FROM tasks WHERE status = 'IN_PROGRESS' планировщик безошибочно выберет точечный Index Scan, прочитав пару индексных страниц за 1 миллисекунду.
Если же статистика устарела (например, база считает, что статусы распределены 50/50), оптимизатор решит, что индекс неэффективен, и запустит Sequential Scan по всей многогигабайтной таблице, превратив быстрый микросервисный запрос в катастрофическую блокировку ввода-вывода!


4. Защита целостности: Visibility Map и предотвращение Wraparound

Помимо базовой сборки мусора, Autovacuum выполняет две жизненно важные архитектурные функции, без которых надежное функционирование СУБД невозможно:

1. Актуализация карты видимости (Visibility Map)

Каждый раз, когда VACUUM обрабатывает страницу и убеждается, что на ней больше нет ни одного мертвого кортежа и незакоммиченных транзакций, он выставляет бит all_visible в файле Visibility Map.
Как мы выяснили в 4. Индексы в PostgreSQL, именно этот бит позволяет планировщику применять сверхбыстрый Index-Only Scan, извлекая данные непосредственно из B-Tree индекса без единого обращения к диску за страницами Heap.

2. Заморозка транзакций (Freezing) и угроза Wraparound

Идентификаторы транзакций xmin/xmax имеют размер 32 бита (лимит около 4 миллиардов значений, закольцованных по модулю $2^{32}$). Если счетчик сделает полный оборот, старые транзакции окажутся «в будущем», и данные базы мгновенно исчезнут для всех правил видимости!
Чтобы исключить эту катастрофу, вакуум регулярно выполняет процедуру Freezing (Заморозку): он инспектирует старые строки и заменяет их xmin на фиксированный служебный маркер FrozenTransactionId (равный 2), который навсегда закрепляет строку как видимую в абсолютном прошлом. Если Autovacuum не успевает за темпом генерации xid, PostgreSQL принудительно аварийно останавливается и переходит в режим read-only для спасения данных от переполнения.


5. Практические советы по тюнингу и конфигурированию

В высоконагруженных системах стандартная конфигурация Autovacuum требует осознанной калибровки:

  1. maintenance_work_mem: Объем оперативной памяти, выделяемый воркеру вакуума для удержания буфера CTID мертвых строк при обходе индексов. Если памяти недостаточно, процессу придется совершать множественные проходы по тяжелым B-Tree деревьям. В промышленных серверах устанавливайте от 512 МБ до 2 ГБ.
  2. autovacuum_max_workers: По умолчанию равен 3. Если в вашем кластере сотни активных таблиц с постоянными вставками и обновлениями, три воркера не справятся с нагрузкой. Увеличьте пул до 5–8 воркеров (при наличии свободных ядер CPU).
  3. autovacuum_vacuum_cost_limit: Ограничитель бюджета дискового ввода-вывода. По умолчанию вакуум делает паузы, чтобы не мешать пользовательским запросам. Если вакуум отстает от потока операций UPDATE из вашего Go-сервиса, увеличьте этот лимит (например, с 200 до 1000–2000 единиц) или снизьте задержку autovacuum_vacuum_cost_delay.

Резюме для инженера

  1. VACUUM освобождает страницы изнутри: Он не уменьшает размер файлов ОС, но открывает слоты для новых INSERT и UPDATE.
  2. ANALYZE питает планировщик: Без актуальной статистики кардинальности и гистограмм распределения база неизбежно начнет деградировать на неоптимальных планах.
  3. Агрессивный Autovacuum: Снижайте scale_factor на больших таблицах, чтобы не накапливать миллионы мертвых строк.
  4. Visibility Map и Freeze: Регулярный вакуум делает возможным Index-Only Scan и предотвращает катастрофу Transaction ID Wraparound.
  5. Транзакционная гигиена в Go: Строго контролируйте таймауты контекста и не допускайте долгоживущих открытых транзакций.

Мы завершили детальный разбор внутреннего устройства одиночного сервера PostgreSQL: от физического слоя страниц и MVCC до тонкостей планировщика и фоновой сборки мусора. Однако надежность критически важных информационных систем никогда не доверяется одной машине. В заключительной лекции раздела мы разберем, как объединять серверы в отказоустойчивые кластеры: 11. Репликация в PostgreSQL.