# Автоочистка

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<span id="internal-ru-chapter-7" class="pdf-anchor"></span><span id="internal-ru-page-139" class="pdf-anchor"></span>

## 7.1. Автоматическая очистка и анализ {#internal-ru-section-7-1}

<span id="internal-ru-dummy-61" class="pdf-anchor"></span>

Когда горизонт базы данных регулярно продвигается вперед, обычная очистка должна справляться со своей работой. Вопрос в том, как часто ее следует вызывать.

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

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

К тому же нагрузка может изменяться со временем, так что запуск очистки по какому-либо фиксированному расписанию в любом случае не годится: чем чаще обновляется таблица, тем чаще ее надо и очищать.

Задача решается *автоматической очисткой* [^internal-ru-139-1] — механизмом, который подстраивает частоту срабатывания очистки и анализа под скорость изменения таблиц.

<span id="internal-ru-section-star-105" class="pdf-anchor"></span>

### Устройство автоочистки {#internal-ru-subsection-star-106}

<span id="internal-ru-page-140" class="pdf-anchor"></span><span id="internal-ru-dummy-62" class="pdf-anchor"></span>

При включенной автоматической очистке (конфигурационный параметр *autovacuum*) в системе всегда присутствует процесс autovacuum launcher, ответственный за расписание. Он поддерживает список баз данных, в которых есть какая-либо активность, опираясь на статистику их использования. Такая статистика собирается при установленном параметре *track\_counts*. Не следует выключать эти параметры, иначе автоочистка не будет работать. (*по умолчанию:* `on`) (*по умолчанию:* `on`)

Раз в *autovacuum\_naptime* единиц времени процесс autovacuum launcher запускает (как обычно, с помощью postmaster) рабочий процесс autovacuum worker [^internal-ru-140-1] для каждой «активной» базы данных из списка. Соответственно, если в кластере используется несколько баз данных (*N* штук), то за интервал *autovacuum\_naptime* будет запущено *N* рабочих процессов. Но при этом общее количество одновременно выполняющихся рабочих процессов ограничено значением параметра *autovacuum\_max\_workers*. (*по умолчанию:* `1min`) (*по умолчанию:* `3`)

Ограничение количества рабочих процессов можно менять на лету, но только в пределах *autovacuum\_worker\_slots*. А изменение количества слотов уже требует перезапуска сервера, поскольку резервирует место в его разделяемой памяти. (v. 18) (*по умолчанию:* `16`) (*с.* [379](/ru/18-table-access-methods/#internal-ru-page-380))

> [!NOTE]
> Рабочие процессы автоочистки очень напоминают фоновые рабочие процессы, но появились задолго до того, как был создан этот механизм заданий общего вида. Реализацию процессов автоочистки не стали менять, поэтому они не задействуют слоты

из *max\_worker\_processes*.

Запустившись, рабочий процесс подключается к указанной ему базе данных и начинает с того, что строит два списка:

- список всех таблиц, материализованных представлений и toast-таблиц, требующих очистки;

- список всех таблиц и материализованных представлений, требующих анализа (toast-таблицы не анализируются, потому что обращение к ним всегда происходит по индексу).

Затем рабочий процесс по очереди очищает или анализирует отобранные объекты (или выполняет и то, и другое), после чего завершается.

<span id="internal-ru-page-141" class="pdf-anchor"></span>

Очистка каждой таблицы происходит так же, как при запуске команды `VACUUM` вручную. Но есть некоторые отличия:

- Очистка, запускаемая вручную, использует для накопления идентификаторов версий фрагмент памяти размером *maintenance\_work\_mem*.

При автоматической очистке одновременно выполняются несколько рабочих процессов, и оперативная память нужна каждому. Поэтому для автоочистки предусмотрен отдельный параметр *autovacuum\_work\_mem*. Значение по умолчанию, при котором используется обычное ограничение *maintenance\_work\_mem*, скорее всего, следует скорректировать, если конфигурация предполагает большое количество рабочих процессов (*autovacuum\_max\_workers*). (*по умолчанию:* `-1`)

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

Если процесс не успеет выполнить всю намеченную работу за интервал времени *autovacuum\_naptime*, процесс autovacuum launcher пошлет в ту же базу данных еще один рабочий процесс, и они будут работать вместе. Второй процесс построит свои собственные списки объектов для очистки и анализа и пойдет по ним. Параллельно будут обрабатываться *разные* таблицы; на уровне одной таблицы параллелизма нет.

<span id="internal-ru-section-star-107" class="pdf-anchor"></span>

### Какие таблицы требуют очистки {#internal-ru-subsection-star-108}

Автоочистку можно отключить на уровне отдельных таблиц, хотя сложно придумать причину, по которой это было бы необходимо. За отключение отвечают два параметра хранения: один для простых таблиц, другой — для toast-таблиц:

- *autovacuum\_enabled*;

- *toast.autovacuum\_enabled*.

Обычно же автоочистка срабатывает при накоплении либо неактуальных версий, либо новых строк. (*с.* [153](/ru/07-autovacuum/#internal-ru-page-154))

<span id="internal-ru-page-142" class="pdf-anchor"></span><span id="internal-ru-section-star-109" class="pdf-anchor"></span><span id="internal-ru-paragraph-star-110" class="pdf-anchor"></span>

**Неактуальные версии строк.** Примерное число неактуальных версий постоянно отслеживается и доступно в таблице `pg_stat_all_tables`.

Считается, что очистка необходима, если количество неактуальных, мертвых версий строк превышает пороговое значение, определяемое тремя параметрами:

- *autovacuum\_vacuum\_threshold* — абсолютное значение (в штуках); (*по умолчанию:* `50`)

- *autovacuum\_vacuum\_scale\_factor* — доля строк в таблице; (*по умолчанию:* `0.2`)

- *autovacuum\_vacuum\_max\_threshold* — дополнительный порог для очень больших таблиц. (*по умолчанию:* `100 млн`) (v. 18)

Формула такова: очистка мертвых версий требуется, если

$$
\begin{gathered}\texttt{pg\_stat\_all\_tables.n\_dead\_tup} > \\ \mathit{autovacuum\_vacuum\_threshold} + \\ \mathit{autovacuum\_vacuum\_scale\_factor} \times \texttt{pg\_class.reltuples}\end{gathered}
$$

или (v. 18)

$$
\begin{gathered}\texttt{pg\_stat\_all\_tables.n\_dead\_tup} > \\ \mathit{autovacuum\_vacuum\_max\_threshold}.\end{gathered}
$$

В первой части формулы для больших таблиц (а именно с ними и связаны возможные проблемы) важен параметр *autovacuum\_vacuum\_scale\_factor*. Значение по умолчанию 20 % представляется сильно завышенным; скорее всего, его потребуется существенно уменьшить. Если и после этого пороговое значение слишком велико для очень больших таблиц, его можно ограничить сверху параметром из второй части формулы.

Если не удается подобрать универсальные значения параметров, их можно переопределить для отдельных таблиц с помощью одноименных параметров хранения (*autovacuum\_vacuum\_threshold*, *autovacuum\_vacuum\_scale\_factor*, *autovacuum\_vacuum\_max\_threshold*). Аналогичные параметры хранения с префиксом *toast* определяют настройки для toast-таблиц.

<span id="internal-ru-page-143" class="pdf-anchor"></span><span id="internal-ru-section-star-111" class="pdf-anchor"></span><span id="internal-ru-dummy-63" class="pdf-anchor"></span><span id="internal-ru-paragraph-star-112" class="pdf-anchor"></span>

**Вставленные строки.** Для хранилищ данных типична ситуация, когда строки добавляются, но никогда не удаляются и не изменяются. В этом случае в таблице не будет мертвых версий и предыдущее условие срабатывания не будет выполняться. Однако и такие таблицы необходимо очищать, чтобы заблаговременно замораживать версии строк и обновлять карту видимости (и тем самым разрешать сканирование только индекса). Поэтому добавляется еще одно условие. (v. 13) (*с.* [422](/ru/20-index-scans/#internal-ru-page-423))

Очистка будет выполняться, если число строк, вставленных с момента последней очистки, превышает пороговое значение, определяемое парой параметров:

- *autovacuum\_vacuum\_insert\_threshold*; (*по умолчанию:* `1000`)

- *autovacuum\_vacuum\_insert\_scale\_factor*. (*по умолчанию:* `0.2`)

Как и число неактуальных версий, количество вставленных строк отслеживается как часть статистики использования таблицы.

Формула такова:

$$
\begin{gathered}\texttt{pg\_stat\_all\_tables.n\_ins\_since\_vacuum} > \\ \mathit{autovacuum\_vacuum\_insert\_threshold} + \\ \mathit{autovacuum\_vacuum\_insert\_scale\_factor} \times \texttt{pg\_class.reltuples} \times \\ \left(1-\texttt{pg\_class.relallfrozen}/\texttt{pg\_class.relpages}\right).\end{gathered}
$$

Если с первым слагаемым все понятно — это просто абсолютное значение, — то ко второму необходим комментарий. В нем вычисляется доля не от всех, а только от *незамороженных* строк; соответствующая часть таблицы оценивается отношением числа полностью замороженных страниц (`relallfrozen`) к их общему количеству (`relpages`). Дело в том, что с ростом таблицы строк в ней становится все больше и больше, и без коэффициента очистка выполнялась бы все реже и реже. А вот количество незамороженных строк периодически уменьшается, поэтому частота срабатываний не увеличивается — что хорошо. (v. 18)

Как и в предыдущем случае, мы можем переопределить значения параметров для отдельных таблиц с помощью одноименных параметров хранения (*autovacuum\_vacuum\_insert\_threshold* и *autovacuum\_vacuum\_insert\_scale\_factor*) и их toast-версий.

<span id="internal-ru-page-144" class="pdf-anchor"></span><span id="internal-ru-section-star-113" class="pdf-anchor"></span>

### Какие таблицы требуют анализа {#internal-ru-subsection-star-114}

С автоанализом дело обстоит проще, чем с очисткой, потому что учитываются только измененные строки.

Считается, что анализа требуют те таблицы, в которых число измененных строк (с момента прошлого анализа) превышает пороговое значение, заданное парой параметров:

- *autovacuum\_analyze\_threshold*; (*по умолчанию:* `50`)

- *autovacuum\_analyze\_scale\_factor*. (*по умолчанию:* `0.1`)

Формула срабатывания автоанализа:

$$
\begin{gathered}\texttt{pg\_stat\_all\_tables.n\_mod\_since\_analyze} > \\ \mathit{autovacuum\_analyze\_threshold} + \\ \mathit{autovacuum\_analyze\_scale\_factor} \times \texttt{pg\_class.reltuples}.\end{gathered}
$$

Настройки автоанализа также можно переопределить на уровне таблиц с помощью одноименных параметров хранения (*autovacuum\_analyze\_threshold* и *autovacuum\_analyze\_scale\_factor*). Поскольку toast-таблицы не анализируются, для них соответствующих параметров нет.

<span id="internal-ru-section-star-115" class="pdf-anchor"></span>

### Автоочистка в действии {#internal-ru-subsection-star-116}

Начнем с написания функции, возвращающей текущее значение параметра:

```text {label="psql"}
=> CREATE FUNCTION p(param text, c pg_class) RETURNS float
AS $$
  SELECT coalesce(
    -- если параметр хранения задан, то берем его
    (SELECT option_value
     FROM   pg_options_to_table(c.reloptions)
     WHERE  option_name = CASE -- для toast-таблиц имя отличается
              WHEN c.relkind = 't' THEN 'toast.' ELSE ''
            END || param
    ),
    -- иначе берем значение конфигурационного параметра
    current_setting(param)
  )::float;
$$ LANGUAGE sql;
```

<span id="internal-ru-page-145" class="pdf-anchor"></span>

Мы используем эту функцию в представлениях, которые покажут нам, какие таблицы в данный момент нуждаются в очистке и анализе[^internal-ru-145-1]. Представления пригодятся в примерах, а заодно формализуют все сказанное выше в этом разделе.

Так может выглядеть представление для очистки:

```text {label="psql"}
=> CREATE VIEW need_vacuum AS
WITH c AS (
  SELECT c.oid,
    greatest(c.reltuples, 0) AS reltuples,
    CASE
      WHEN c.relpages > 0 THEN
        1 - c.relallfrozen::float / c.relpages
      ELSE 1
    END AS pct_unfrozen,
    p('autovacuum_vacuum_threshold', c) AS threshold,
    p('autovacuum_vacuum_scale_factor', c) AS scale_factor,
    p('autovacuum_vacuum_max_threshold', c) AS max_threshold,
    p('autovacuum_vacuum_insert_threshold', c) AS ins_threshold,
    p('autovacuum_vacuum_insert_scale_factor', c) AS ins_scale_factor
  FROM pg_class c
  WHERE c.relkind IN ('r','m','t')
),
r AS (
  SELECT st.schemaname || '.' || st.relname AS tablename,
    st.n_dead_tup AS dead_tup,
    least(
      c.threshold + c.scale_factor * c.reltuples,
      c.max_threshold
    ) AS max_dead_tup,
    st.n_ins_since_vacuum AS ins_tup,
    c.ins_threshold + c.ins_scale_factor * c.reltuples * c.pct_unfrozen
      AS max_ins_tup,
    st.last_autovacuum
  FROM pg_stat_all_tables st
    JOIN c ON c.oid = st.relid
)
SELECT *,
  CASE
    WHEN dead_tup > max_dead_tup OR ins_tup > max_ins_tup
      THEN 'Пора!'
  END AS is_it_time
FROM r;
```

<span id="internal-ru-page-146" class="pdf-anchor"></span>

Столбец `max_dead_tup` показывает пороговое значение срабатывания автоочистки из-за мертвых версий строк, а столбец `max_ins_tup` — из-за вставки новых строк.

А вот аналогичное представление для анализа:

```text {label="psql"}
=> CREATE VIEW need_analyze AS
WITH c AS (
  SELECT c.oid,
    greatest(c.reltuples, 0) AS reltuples,
    p('autovacuum_analyze_threshold', c) AS threshold,
    p('autovacuum_analyze_scale_factor', c) AS scale_factor
  FROM pg_class c
  WHERE c.relkind IN ('r','m')
),
r AS (
  SELECT st.schemaname || '.' || st.relname AS tablename,
    st.last_autoanalyze,
    st.n_mod_since_analyze AS mod_tup,
    c.threshold + c.scale_factor * c.reltuples AS max_mod_tup
  FROM pg_stat_all_tables st
    JOIN c ON c.oid = st.relid
)
SELECT *,
  CASE
    WHEN mod_tup > max_mod_tup THEN 'Пора!'
  END AS is_it_time
FROM r;
```

Столбец `max_mod_tup` показывает пороговое значение срабатывания автоанализа.

Чтобы не ждать долго автоочистку, для экспериментов будем запускать процесс каждую секунду:

```text {label="psql"}
=> ALTER SYSTEM SET autovacuum_naptime = '1s';
=> SELECT pg_reload_conf();
```

Опустошим таблицу `vac` и вставим в нее тысячу строк. Напомню, что автоочистка отключена на уровне таблицы.

```text {label="psql"}
=> TRUNCATE TABLE vac;
=> INSERT INTO vac(id,s)
  SELECT id, 'A' FROM generate_series(1,1000) id;
```

<span id="internal-ru-page-147" class="pdf-anchor"></span>

Вот что покажет наше представление для очистки:

```text {label="psql"}
=> SELECT * FROM need_vacuum WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]---+-----------
tablename       | public.vac
dead_tup        | 0
max_dead_tup    | 50
ins_tup         | 1000
max_ins_tup     | 1000
last_autovacuum |
is_it_time      |
```

Пороговое значение `max_dead_tup` = 50, хотя по приведенной выше формуле должно быть 50 + 0,2 × 1000 = 250. Дело в том, что еще нет статистики по таблице, поскольку команда `INSERT` сама по себе ее не обновляет:

```text {label="psql"}
=> SELECT reltuples FROM pg_class WHERE relname = 'vac';
 reltuples
-----------
        -1
(1 row)
```

Специальная константа −1 для `pg_class.reltuples` позволяет отличать таблицу без статистики от пустой, но проанализированной таблицы, для которой этот столбец будет равен нулю. Для расчетов отрицательное значение приводится к нулю, что и дает 50 + 0,2 × 0 = 50. (v. 14)

Значение `max_ins_tup` = 1000 отличается от расчетного числа 1200 по той же причине.

Заглянем в представление для анализа:

```text {label="psql"}
=> SELECT * FROM need_analyze WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]----+-----------
tablename        | public.vac
last_autoanalyze |
mod_tup          | 1006
max_mod_tup      | 50
is_it_time       | Пора!
```

Мы изменили тысячу строк (в данном случае они были добавлены), и это больше порога, который пока равен 50 с учетом неизвестного размера таблицы. Значит, автоанализ должен сработать.

<span id="internal-ru-page-148" class="pdf-anchor"></span>

Включим его:

```text {label="psql"}
=> ALTER TABLE vac SET (autovacuum_enabled = on);
```

После небольшой паузы таблица оказывается проанализированной, пороговое значение исправляется на корректные 150 строк:

```text {label="psql"}
=> SELECT reltuples FROM pg_class WHERE relname = 'vac';
 reltuples
-----------
      1000
(1 row)
=> SELECT * FROM need_analyze WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]----+------------------------------
tablename        | public.vac
last_autoanalyze | 2026-04-05 20:52:06.591521+03
mod_tup          | 0
max_mod_tup      | 150
is_it_time       |
```

Вернемся к автоочистке:

```text {label="psql"}
=> SELECT * FROM need_vacuum WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]---+-----------
tablename       | public.vac
dead_tup        | 0
max_dead_tup    | 250
ins_tup         | 1000
max_ins_tup     | 1200
last_autovacuum |
is_it_time      |
```

Значения `max_dead_tup` и `max_ins_tup` тоже исправились с учетом выясненного в процессе анализа размера таблицы.

На данный момент очистка может состояться в одном из двух случаев:

- при появлении более 250 мертвых версий строк;

- при вставке в таблицу более 200 строк. (v. 13)

Снова отключим автоочистку и обновим 251 строку — на одну больше, чем пороговое значение:

<span id="internal-ru-page-149" class="pdf-anchor"></span>

```text {label="psql"}
=> ALTER TABLE vac SET (autovacuum_enabled = off);
=> UPDATE vac SET s = 'B' WHERE id % 4 = 0 OR id = 1;
UPDATE 251
=> SELECT * FROM need_vacuum WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]---+-----------
tablename       | public.vac
dead_tup        | 251
max_dead_tup    | 250
ins_tup         | 1000
max_ins_tup     | 1200
last_autovacuum |
is_it_time      | Пора!
```

Теперь условие срабатывания выполняется. Включим автоочистку и после непродолжительной паузы обнаружим, что таблица обработана, а статистика использования сброшена:

```text {label="psql"}
=> ALTER TABLE vac SET (autovacuum_enabled = on);
=> SELECT * FROM need_vacuum WHERE tablename = 'public.vac' \gx
-[ RECORD 1 ]---+------------------------------
tablename       | public.vac
dead_tup        | 0
max_dead_tup    | 250
ins_tup         | 0
max_ins_tup     | 1200
last_autovacuum | 2026-04-05 20:52:11.763126+03
is_it_time      |
```

## 7.2. Заморозка {#internal-ru-section-7-2}

Для экспериментов с заморозкой создадим новую таблицу. Установим для нее минимальное значение параметра *fillfactor* так, чтобы на каждой странице помещалось всего две строки — так будет удобнее наблюдать за происходящим. И как и раньше, отключим автоматику, чтобы управлять временем очистки самостоятельно.

```text {label="psql"}
=> CREATE TABLE tfreeze(
  id integer,
  s char(300)
) WITH (fillfactor = 10, autovacuum_enabled = off);
```

<span id="internal-ru-page-150" class="pdf-anchor"></span>

Очередной вариант функции для просмотра страниц с помощью расширения `pageinspect` будет отображать диапазон страниц, расшифровывать признак заморозки (`f`) и показывать возраст транзакции `xmin` (для этого используется системная функция `age`; сам возраст, конечно, не хранится в страницах данных):

```text {label="psql"}
=> CREATE FUNCTION heap_page(
  relname text, pageno_from integer, pageno_to integer
)
RETURNS TABLE(
  ctid tid, state text,
  xmin text, xmin_age integer, xmax text
) AS $$
SELECT (pageno,lp)::text::tid AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin || CASE
         WHEN (t_infomask & 256+512) = 256+512 THEN ' f'
         WHEN (t_infomask & 256) > 0 THEN ' c'
         WHEN (t_infomask & 512) > 0 THEN ' a'
         ELSE ''
       END AS xmin,
       age(t_xmin) AS xmin_age,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' c'
         WHEN (t_infomask & 2048) > 0 THEN ' a'
         ELSE ''
       END AS xmax
FROM generate_series(pageno_from, pageno_to) p(pageno),
     heap_page_items(get_raw_page(relname, pageno))
ORDER BY pageno, lp;
$$ LANGUAGE sql;
```

Вставляем в таблицу некоторое количество строк и сразу выполняем очистку, чтобы создалась карта видимости:

```text {label="psql"}
=> CREATE EXTENSION IF NOT EXISTS pg_visibility;
=> INSERT INTO tfreeze(id, s)
  SELECT id, 'FOO'||id FROM generate_series(1,100) id;
INSERT 0 100
=> VACUUM tfreeze;
```

<span id="internal-ru-page-151" class="pdf-anchor"></span>

Будем наблюдать за первыми двумя страницами с помощью расширения `pg_visibility`. После очистки обе страницы оказываются отмеченными в карте видимости (`all_visible`), но не в карте заморозки (`all_frozen`) — они еще содержат незамороженные версии строк:

```text {label="psql"}
=> SELECT *
FROM generate_series(0,1) g(blkno),
     pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | f
     1 | t           | f
(2 rows)
```

У транзакции, создавшей строки, возраст `xmin_age` равен 1, поскольку это последняя транзакция, которая выполнялась в системе:

```text {label="psql"}
=> SELECT * FROM heap_page('tfreeze',0,1);
 ctid  | state  | xmin  | xmin_age | xmax
-------+--------+-------+----------+------
 (0,1) | normal | 877 c |        1 | 0 a
 (0,2) | normal | 877 c |        1 | 0 a
 (1,1) | normal | 877 c |        1 | 0 a
 (1,2) | normal | 877 c |        1 | 0 a
(4 rows)
```

## 7.3. Управление заморозкой {#internal-ru-section-7-3}

Заморозкой управляют четыре основных конфигурационных параметра. Значения всех четырех представляют возраст транзакций и определяют моменты, начиная с которых:

- версии начинают замораживаться — *vacuum\_freeze\_min\_age*;

- выполняется агрессивная заморозка — *vacuum\_freeze\_table\_age*;

- заморозка срабатывает аварийно — *autovacuum\_freeze\_max\_age*;

- заморозка работает в приоритетном режиме — *vacuum\_failsafe\_age*. (v. 14)

<span id="internal-ru-page-152" class="pdf-anchor"></span><span id="internal-ru-section-star-117" class="pdf-anchor"></span>

### Минимальный возраст для заморозки {#internal-ru-subsection-star-118}

Минимальный возраст транзакции `xmin`, при котором версию строки можно замораживать, определяется параметром *vacuum\_freeze\_min\_age*. Чем меньше это значение, тем сильнее могут вырасти накладные расходы: если строка окажется «горячей» и будет активно изменяться, то заморозка все новых и новых версий будет пропадать без пользы. Установка относительно большого значения параметра позволяет выждать. (*по умолчанию:* `50 млн`)

> [!NOTE]
> Если вызвать заморозку вручную командой `VACUUM FREEZE`, будут заморожены все версии строк без оглядки на возраст транзакций, как будто *vacuum\_freeze\_min\_age* = 0. Именно поэтому в рассмотренном ранее примере была заморожена буквально только что появившаяся версия.

Чтобы посмотреть, как происходит заморозка, уменьшим значение до единицы: (*с.* [136](/ru/06-vacuum-and-freezing/#internal-ru-page-137))

```text {label="psql"}
=> ALTER SYSTEM SET vacuum_freeze_min_age = 1;
=> SELECT pg_reload_conf();
```

Обновим теперь одну строку на нулевой странице. Новая версия попадет на ту же страницу благодаря небольшому значению *fillfactor*:

```text {label="psql"}
=> UPDATE tfreeze SET s = 'BAR' WHERE id = 1;
```

Возраст транзакций увеличился на единицу. Табличные страницы выглядят сейчас следующим образом:

```text {label="psql"}
=> SELECT * FROM heap_page('tfreeze',0,1);
 ctid  | state  | xmin  | xmin_age | xmax
-------+--------+-------+----------+------
 (0,1) | normal | 877 c |        2 | 878
 (0,2) | normal | 877 c |        2 | 0 a
 (0,3) | normal | 878   |        1 | 0 a
 (1,1) | normal | 877 c |        2 | 0 a
 (1,2) | normal | 877 c |        2 | 0 a
(5 rows)
```

Теперь строки старше *vacuum\_freeze\_min\_age* = 1 подлежат заморозке. Но очистка рассматривает только те страницы, которые не отмечены в карте видимости: (*с.* [131](/ru/06-vacuum-and-freezing/#internal-ru-page-132))

<span id="internal-ru-page-153" class="pdf-anchor"></span>

```text {label="psql" hl_lines="7"}
=> SELECT *
FROM generate_series(0,1) g(blkno),
     pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | f           | f
     1 | t           | f
(2 rows)
```

> [!NOTE]
> Очистка пропускает страницы, отмеченные в карте видимости, только если подряд идут минимум 32 такие страницы[^internal-ru-153-1]. Если же признаки чередуются чаще, карта видимости игнорируется: пропускать отдельные страницы не имеет большого смысла, поскольку они все равно окажутся в оперативной памяти благодаря предвыборке. Именно поэтому таблица `tfreeze` создана достаточно большой, хотя нас интересуют только две страницы.

Предыдущая команда `UPDATE` сбросила бит видимости нулевой страницы, а вот первую страницу очистка пропустит:

```text {label="psql" hl_lines="7 8"}
=> VACUUM tfreeze;
VACUUM
=> SELECT * FROM heap_page('tfreeze',0,1);
 ctid  |     state     | xmin  | xmin_age | xmax
-------+---------------+-------+----------+------
 (0,1) | redirect to 3 |       |          |
 (0,2) | normal        | 877 f |        2 | 0 a
 (0,3) | normal        | 878 f |        1 | 0 a
 (1,1) | normal        | 877 c |        2 | 0 a
 (1,2) | normal        | 877 c |        2 | 0 a
(5 rows)
```

Обратите внимание, что на нулевой странице были заморожены обе версии строк, хотя возраст транзакции `xmin` во второй версии не превышает значение параметра *vacuum\_freeze\_min\_age*. Поскольку страницу все равно приходится менять, на ней замораживаются все версии строк за горизонтом базы данных — это практически не увеличивает стоимость операции, но может сэкономить усилия в будущем. (v. 16)

<span id="internal-ru-page-154" class="pdf-anchor"></span>

Теперь обе страницы снова отмечены в карте видимости. Несмотря на то что первая страница содержит незамороженные строки, очистка будет пропускать ее, если в ней ничего не изменится:

```text {label="psql" hl_lines="7"}
=> SELECT * FROM generate_series(0,1) g(blkno),
     pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | t
     1 | t           | f
(2 rows)
```

<span id="internal-ru-section-star-119" class="pdf-anchor"></span>

### Возраст для агрессивной заморозки {#internal-ru-subsection-star-120}

<span id="internal-ru-dummy-64" class="pdf-anchor"></span>

Итак, если на странице остались только актуальные версии, видимые во всех снимках, очистка не заморозит их. Справиться с проблемой позволяет параметр *vacuum\_freeze\_table\_age*. Его значение определяет возраст транзакции, при котором очистка игнорирует карту видимости и замораживает все страницы, предотвращая последствия переполнения счетчика транзакций. Такой режим очистки называется агрессивным; поскольку его основная задача состоит в заморозке, я буду употреблять и термин «агрессивная заморозка». (*по умолчанию:* `150 млн`) (*с.* [134](/ru/06-vacuum-and-freezing/#internal-ru-page-135))

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

```text {label="psql"}
=> SELECT relfrozenxid, age(relfrozenxid)
FROM pg_class WHERE relname = 'tfreeze';
 relfrozenxid | age
--------------+-----
          877 |   2
(1 row)
```

С возрастом этой запомненной транзакции и сравнивается значение параметра *vacuum\_freeze\_table\_age* для принятия решения об агрессивной заморозке.

<span id="internal-ru-page-155" class="pdf-anchor"></span>

Благодаря карте заморозки нет необходимости полностью сканировать таблицу при очистке; достаточно просмотреть только те страницы, которые еще не отмечены в карте. Карта заморозки не только существенно уменьшает объем работы, но и дает устойчивость к прерываниям: если процесс очистки остановить и начать заново, ему не придется возвращаться к страницам, которые уже были обработаны и отмечены в карте в прошлый раз.

Для заморозки всей таблицы уменьшим значение *vacuum\_freeze\_table\_age* до двух, чтобы выполнилось условие агрессивной заморозки:

```text {label="psql"}
=> ALTER SYSTEM SET vacuum_freeze_table_age = 2;
=> SELECT pg_reload_conf();
```

Выполним очистку:

```text {label="psql" hl_lines="2 7"}
=> VACUUM VERBOSE tfreeze;
INFO:  aggressively vacuuming "internals.public.tfreeze"
INFO:  finished vacuuming "internals.public.tfreeze": index scans: 0
pages: 0 removed, 50 remain, 50 scanned (100.00% of total), 0 eagerly
scanned
tuples: 0 removed, 100 remain, 0 are dead but not yet removable
removable cutoff: 879, which was 0 XIDs old when operation ended
new relfrozenxid: 879, which is 2 XIDs ahead of previous value
frozen: 48 pages from table (96.00% of total) had 96 tuples frozen
visibility map: 0 pages set all-visible, 48 pages set all-frozen (48
were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead
item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 116 hits, 0 reads, 0 dirtied
WAL usage: 97 records, 0 full page images, 6615 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
```

Теперь, поскольку агрессивная очистка проверяет всю таблицу, номер замороженной транзакции можно увеличить — в страницах гарантированно не осталось более старой незамороженной транзакции:

```text {label="psql"}
=> SELECT relfrozenxid, age(relfrozenxid)
FROM pg_class WHERE relname = 'tfreeze';
 relfrozenxid | age
--------------+-----
          879 |   0
(1 row)
```

Разумеется, на первой странице теперь заморожены все версии строк:

<span id="internal-ru-page-156" class="pdf-anchor"></span>

```text {label="psql" hl_lines="7 8"}
=> SELECT * FROM heap_page('tfreeze',0,1);
 ctid  |     state     | xmin  | xmin_age | xmax
-------+---------------+-------+----------+------
 (0,1) | redirect to 3 |       |          |
 (0,2) | normal        | 877 f |        2 | 0 a
 (0,3) | normal        | 878 f |        1 | 0 a
 (1,1) | normal        | 877 f |        2 | 0 a
 (1,2) | normal        | 877 f |        2 | 0 a
(5 rows)
```

Кроме того, первая страница отмечена в карте заморозки:

```text {label="psql" hl_lines="7"}
=> SELECT * FROM generate_series(0,1) g(blkno),
  pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | t
     1 | t           | t
(2 rows)
```

Агрессивная заморозка всех страниц в таблице выполняется не чаще, чем раз в *vacuum\_freeze\_table\_age* − *vacuum\_freeze\_min\_age* транзакций (100 млн при значениях параметров по умолчанию). Она будет запускаться реже, если очистка в неагрессивном режиме тоже сможет продвинуть значение `relfrozenxid`[^internal-ru-156-1]. В любом случае надо учитывать, что слишком большое значение параметра *vacuum\_freeze\_min\_age* вместо экономии может привести к избыточным срабатываниям агрессивной заморозки и увеличению накладных расходов. (v. 15)

<span id="internal-ru-section-star-121" class="pdf-anchor"></span>

### Возраст для аварийного срабатывания автоочистки {#internal-ru-subsection-star-122}

<span id="internal-ru-page-157" class="pdf-anchor"></span>

Бывают ситуации, когда предыдущих двух настроек оказывается недостаточно для своевременной заморозки версий строк. Автоочистку можно отключить, а обычную очистку — не запускать (так делать не надо, но технически это возможно). Также могут присутствовать таблицы, не попадающие в очередь автоочистки, или неактивные базы данных (например, `template0`), в которые автоочистка вообще не приходит. Для подобных случаев предусмотрено *аварийное* срабатывание автоочистки в агрессивном режиме, что позволяет предотвратить переполнение счетчика транзакций. (*с.* [139](/ru/07-autovacuum/#internal-ru-page-140))

Агрессивная очистка базы данных запускается аварийно, когда возраст какой-нибудь незамороженной транзакции в этой базе может превысить значение *autovacuum\_freeze\_max\_age*[^internal-ru-157-1]. Даже если отключить автоочистку, она все равно запустится — правда, не сразу, а при первом ручном выполнении очистки или перезапуске сервера. (*по умолчанию:* `200 млн`)

Определяющим для аварийного срабатывания является возраст самой старой из транзакций `pg_class.relfrozenxid` по всем таблицам базы, поскольку все еще более старые транзакции гарантированно заморожены. Номер этой транзакции запоминается в системном каталоге:

```text {label="psql"}
=> SELECT datname, datfrozenxid, age(datfrozenxid) FROM pg_database;
  datname  | datfrozenxid | age
-----------+--------------+-----
 postgres  |          744 | 135
 template1 |          744 | 135
 template0 |          744 | 135
 internals |          744 | 135
(4 rows)
```

`datfrozenxid`

![Возраст для аварийного срабатывания автоочистки: все версии; строк в базе данных; гарантированно заморожены; xid; relfrozenxidrelfrozenxidrelfrozenxid; таблицы 1таблицы 3таблицы 2](/images/internal/ru/p157-01.svg)
{#internal-ru-fig-7-1 num="7.1" caption="Возраст для аварийного срабатывания автоочистки" .book-original-figure width=692 height=363}

<span id="internal-ru-page-158" class="pdf-anchor"></span>

Предел для значения *autovacuum\_freeze\_max\_age* составляет 2 млрд транзакций (чуть менее половины круга), а значение по умолчанию в 10 раз меньше. В этом есть смысл: большое значение увеличивает риск того, что за оставшееся до переполнения счетчика время автоочистка просто не успеет заморозить все необходимые версии строк. В этом случае PostgreSQL аварийно остановится, чтобы предотвратить возможные проблемы, и запуск потребует вмешательства администратора.

<span id="internal-ru-dummy-65" class="pdf-anchor"></span>

Кроме того, значение этого параметра ограничивает размер структуры clog. В кластере не может остаться более старых незамороженных транзакций, чем старейшая из `datfrozenxid` по всем базам данных, а для замороженных транзакций статус хранить не нужно. Лишние файлы-сегменты clog удаляются автоочисткой[^internal-ru-158-1]. (*с.* [85](/ru/03-pages-and-tuples/#internal-ru-page-86))

Изменение параметра *autovacuum\_freeze\_max\_age* требует перезапуска сервера. Но при необходимости все рассмотренные выше параметры управления заморозкой можно переопределить с помощью параметров хранения на уровне отдельных таблиц. Обратите внимание на то, что названия всех параметров начинаются на «auto»: *autovacuum\_freeze\_min\_age*, *autovacuum\_freeze\_table\_age* и *autovacuum\_freeze\_max\_age*.

<span id="internal-ru-section-star-123" class="pdf-anchor"></span>

### Возраст для приоритетного режима заморозки {#internal-ru-subsection-star-124}

Если автоочистка уже работает над предотвращением переполнения счетчика транзакций, но явно не успевает, срабатывает «предохранитель»: автоочистка прекращает учитывать задержку *autovacuum\_vacuum\_cost\_delay* (*vacuum\_cost\_delay*) и перестает очищать индексы, чтобы как можно быстрее заморозить версии строк. (v. 14)

Приоритетный режим заморозки включается[^internal-ru-158-2], если возраст какой-нибудь незамороженной транзакции в базе данных может превысить значение параметра *vacuum\_failsafe\_age*. Предполагается, что значение этого параметра должно быть выше, чем значение *autovacuum\_freeze\_max\_age*. (*по умолчанию:* `1,6 млрд`)

<span id="internal-ru-page-159" class="pdf-anchor"></span>

> [!NOTE]
> При ручном вызове, когда цель состоит не в освобождении места на страницах данных, а в максимально быстрой заморозке, очистку индексов тоже имеет смысл отключить. Это можно сделать либо явно, используя команду `VACUUM (freeze,`

`index_cleanup false)`, либо с помощью параметра хранения *vacuum\_index\_cleanup*. (v. 12)

## 7.4. Нетерпеливое сканирование {#internal-ru-section-7-4}

<span id="internal-ru-dummy-66" class="pdf-anchor"></span>

Когда очистка не занимается агрессивной заморозкой, она пропускает страницы, отмеченные в карте видимости, поскольку вычищать из них нечего. Но из этого правила есть исключения. (v. 18)

Про одно я уже вскользь упоминал: очистка *вынуждена* сканировать страницы, отмеченные в карте видимости, если не набирается хотя бы 32 страницы, идущие подряд. Пропускать такие небольшие группы не только бесполезно, но и вредно: может сбиться алгоритм операционной системы, отвечающий за предвыборку страниц.

Другое исключение более интересно. Очистка может *намеренно* просканировать некоторое количество страниц, отмеченных в карте видимости (но не в карте заморозки). Такое сканирование называется «нетерпеливым» (eager).

И в том, и в другом случае к прочитанным страницам применяются обычные правила заморозки, что дает шанс заморозить все версии строк на странице, отметить ее в карте заморозки и таким образом уменьшить объем работы в агрессивном режиме. С другой стороны, чтение лишних страниц — это дополнительная нагрузка, и она не должна мешать основной деятельности.

Если не удается заморозить несколько страниц подряд, то, скорее всего, все версии строк в этом фрагменте таблицы еще недостаточно старые, так что продолжать попытки бесполезно. Размер фрагмента фиксирован (4096 страниц), а доля неудачных попыток ограничена значением параметра *vacuum\_max\_eager\_freeze\_failure\_rate* (или одноименного параметра хранения). Настройки по умолчанию дают право на 122 промаха, после чего нетерпеливое сканирование текущего фрагмента прерывается. (*по умолчанию:* `0.03`)

<span id="internal-ru-page-160" class="pdf-anchor"></span>

Но в другом месте таблицы ситуация может отличаться, поэтому при входе в следующий фрагмент счетчик неудачных попыток сбрасывается и нетерпеливое сканирование продолжается.

Таким образом, в худшем случае нетерпеливое сканирование зря прочитает *vacuum\_max\_eager\_freeze\_failure\_rate* от количества страниц, отмеченных в карте видимости, но не отмеченных в карте заморозки. Установка этого параметра в ноль фактически запрещает нетерпеливое сканирование.

Но и при удачном стечении обстоятельств замораживать сразу много страниц не нужно, чтобы не сильно увеличивать время работы и не превращать обычную очистку в агрессивную. Поэтому число успешных попыток тоже ограничено и не может превышать 20 % от количества страниц, отмеченных в карте видимости, но не в карте заморозки.

Чтобы посмотреть на заморозку при нетерпеливом сканировании, вернем исходное значение возраста агрессивной заморозки, но сохраним установленный ранее минимальный возраст срабатывания обычной очистки.

```text {label="psql"}
=> ALTER SYSTEM RESET vacuum_freeze_table_age;
=> SELECT pg_reload_conf();
=> SHOW vacuum_freeze_min_age;
 vacuum_freeze_min_age
-----------------------
 1
(1 row)
```

Нетерпеливое сканирование происходит не всегда; одно из условий — не слишком маленькая таблица, минимум 8192 страницы. Поэтому наполним `tfreeze` заново достаточным количеством строк (напомню, что на одной странице помещается две версии):

```text {label="psql"}
=> TRUNCATE tfreeze;
=> INSERT INTO tfreeze(id, s)
SELECT id, 'FOO'||id FROM generate_series(1,8192*2) id;
INSERT 0 16384
```

Затем очистим таблицу, чтобы все страницы отметились в карте видимости. И финальный подготовительный штрих: выполним транзакцию, чтобы увеличить возраст всех версий на единицу — теперь они попадают под условие заморозки.

<span id="internal-ru-page-161" class="pdf-anchor"></span>

```text {label="psql"}
=> VACUUM tfreeze;
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                881
(1 row)
```

```text {label="psql"}
=> SELECT * FROM heap_page('tfreeze',0,8191);
   ctid   | state  | xmin  | xmin_age | xmax
----------+--------+-------+----------+------
 (0,1)    | normal | 880 c |        2 | 0 a
 (0,2)    | normal | 880 c |        2 | 0 a
 (1,1)    | normal | 880 c |        2 | 0 a
 (1,2)    | normal | 880 c |        2 | 0 a
 ...
 (8191,1) | normal | 880 c |        2 | 0 a
 (8191,2) | normal | 880 c |        2 | 0 a
(16384 rows)
```

```text {label="psql"}
=> SELECT * FROM generate_series(0,8191) g(blkno),
    pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | f
     1 | t           | f
 ...
  8191 | t           | f
(8192 rows)
```

Наконец, все готово, запускаем очистку с параметром `verbose`:

<span id="internal-ru-page-162" class="pdf-anchor"></span>

```text {label="psql" hl_lines="3 4 10 11"}
=> VACUUM VERBOSE tfreeze;
INFO:  vacuuming "internals.public.tfreeze"
INFO:  disabling eager scanning after freezing 1638 eagerly scanned
blocks of relation "internals.public.tfreeze"
INFO:  finished vacuuming "internals.public.tfreeze": index scans: 0
pages: 0 removed, 8192 remain, 1640 scanned (20.02% of total), 1639
eagerly scanned
tuples: 0 removed, 16384 remain, 0 are dead but not yet removable
removable cutoff: 882, which was 0 XIDs old when operation ended
frozen: 1640 pages from table (20.02% of total) had 3280 tuples frozen
visibility map: 0 pages set all-visible, 1640 pages set all-frozen
(1640 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead
item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 3299 hits, 0 reads, 0 dirtied
WAL usage: 3281 records, 0 full page images, 215167 bytes, 0 buffers
full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
```

Мы видим, что нетерпеливое сканирование было прервано после успешной заморозки 1638 страниц: это как раз 20 % от 8192. Заметьте, что несколькими строками ниже сказано, что всего было заморожено 1640 страниц: как я говорил, нетерпеливое сканирование — не единственный случай, когда обычная очистка читает страницы, отмеченные в карте видимости. Например, последняя страница сканируется всегда: если в ней есть версии строк, очистка не будет пытаться усекать таблицу, чтобы не захватывать впустую исключительную блокировку. (*с.* [133](/ru/06-vacuum-and-freezing/#internal-ru-page-134))

```sql
SELECT * FROM generate_series(0,8191) g(blkno),
    pg_visibility_map('tfreeze',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | t
     1 | t           | t
 ...
  1637 | t           | t
  1638 | t           | t
  1639 | t           | f
  1640 | t           | f
 ...
  8189 | t           | f
  8190 | t           | f
  8191 | t           | t
(8192 rows)
```

## 7.5. Заморозка при загрузке {#internal-ru-section-7-5}

<span id="internal-ru-dummy-67" class="pdf-anchor"></span>

Данные, которые не должны меняться, можно заморозить сразу при начальной загрузке. Для этого используется команда `COPY` с параметром `FREEZE`.

<span id="internal-ru-page-163" class="pdf-anchor"></span>

Загружать данные с заморозкой можно только в таблицу, созданную или опустошенную (командой `TRUNCATE`) в этой же транзакции. Обе эти операции монопольно блокируют таблицу. Такое ограничение необходимо, поскольку замороженные версии считаются видимыми во всех снимках данных независимо от уровня изоляции; иначе транзакции неожиданно обнаруживали бы свежезамороженные строки прямо по мере их загрузки. Но пока таблица заблокирована, другие транзакции не смогут получить к ней доступ. (*с.* [261](/ru/12-relation-level-locks/#internal-ru-page-262))

Тем не менее формально нарушение изоляции все же возможно. В отдельном сеансе начнем транзакцию с уровнем изоляции Repeatable Read:

```text {label="psql"}
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
=> SELECT 1; -- построен снимок
```

Опустошим таблицу `tfreeze` и в той же транзакции загрузим в нее новые строки. (Если бы читающая транзакция успела обратиться к `tfreeze`, команда `TRUNCATE` была бы заблокирована.)

```text {label="psql"}
=> BEGIN;
=> TRUNCATE tfreeze;
=> COPY tfreeze FROM stdin WITH FREEZE;
1 FOO
2 BAR
3 BAZ
\.
=> COMMIT;
```

Теперь читающая транзакция видит новые данные в нарушение изоляции:

```text {label="psql"}
=> SELECT count(*) FROM tfreeze;
 count
-------
     3
(1 row)
=> COMMIT;
```

Но поскольку загрузка данных вряд ли происходит регулярно, в большинстве случаев это не представляет проблемы.

При загрузке с заморозкой сразу же создается карта видимости, а в заголовках страниц проставляется признак видимости: (v. 14) (*с.* [127](/ru/06-vacuum-and-freezing/#internal-ru-page-128))

<span id="internal-ru-page-164" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT * FROM pg_visibility_map('tfreeze',0);
 all_visible | all_frozen
-------------+------------
 t           | t
(1 row)
=> SELECT flags & 4 > 0 AS all_visible
FROM page_header(get_raw_page('tfreeze',0));
 all_visible
-------------
 t
(1 row)
```

Таким образом, после загрузки с заморозкой очистка уже не будет обрабатывать таблицу (если, конечно, данные в ней останутся неизменными). К сожалению, это пока не работает для toast-таблиц: при загрузке длинных значений очистке придется лишний раз переписать всю toast-таблицу, чтобы проставить признак видимости в заголовки всех страниц. (v. 14)

## 7.6. Регулирование нагрузки и мониторинг {#internal-ru-section-7-6}

Очистка не блокирует другие процессы, поскольку работает постранично, но тем не менее создает нагрузку на систему и может оказывать заметное влияние на производительность.

<span id="internal-ru-dummy-68" class="pdf-anchor"></span>

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

<span id="internal-ru-section-star-125" class="pdf-anchor"></span>

### Ручная очистка {#internal-ru-subsection-star-126}

Очистка выполняет примерно *vacuum\_cost\_limit* условных единиц работы, а затем засыпает на *vacuum\_cost\_delay* единиц времени. Такое чередование работы и ожидания позволяет более или менее равномерно распределить влияние очистки на систему, не допуская пиковых нагрузок. (*по умолчанию:* `200`) (*по умолчанию:* `0`)

<span id="internal-ru-page-165" class="pdf-anchor"></span>

Нулевое значение по умолчанию для *vacuum\_cost\_delay* фактически означает, что (ручная) очистка вовсе не засыпает, так что конкретное значение *vacuum\_cost\_limit* не играет никакой роли. Это сделано из соображения, что если уж администратору пришлось запускать очистку вручную, то он, вероятно, хочет выполнить ее как можно быстрее.

Тем не менее если все-таки установить время сна, то указанный в параметре *vacuum\_cost\_limit* объем работы будет складываться из стоимостей работы со страницами в буферном кеше. Каждое чтение страницы оценивается в *vacuum\_cost\_page\_hit* единиц, если страница нашлась в буферном кеше, и в *vacuum\_cost\_page\_miss* единиц, если не нашлась[^internal-ru-165-1]. Если же страница не была грязной, а в результате работы очистки на ней что-то поменялось, это стоит дополнительных *vacuum\_cost\_page\_dirty* единиц[^internal-ru-165-2]. (*с.* [183](/ru/09-buffer-cache/#internal-ru-page-184)) (*по умолчанию:* `1`) (*по умолчанию:* `2`) (*по умолчанию:* `20`)

Если оставить значение *vacuum\_cost\_limit* по умолчанию, за один цикл очистка может обработать от 200 страниц в лучшем случае (если все они закешированы и в результате работы не появилось ни одной новой грязной) до всего 9 в худшем (если все страницы читаются с диска и становятся грязными).

Команда `VACUUM` с указанием `VERBOSE` выполняет очистку и выводит отчет о проделанной работе, а представление `pg_stat_progress_vacuum` показывает состояние уже запущенного процесса.

Похожее представление есть и для анализа (`pg_stat_progress_analyze`), хотя анализ выполняется быстро и обычно не доставляет проблем. (v. 13)

Вставим в таблицу больше строк и все обновим, чтобы очистка выполнялась ощутимое время:

```text {label="psql"}
=> TRUNCATE vac;
=> INSERT INTO vac(id,s)
  SELECT id, 'A' FROM generate_series(1,5_000_000) id;
=> UPDATE vac SET s  = 'B';
UPDATE 5000000
```

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

<span id="internal-ru-page-166" class="pdf-anchor"></span>

```text {label="psql"}
=> ALTER SYSTEM SET maintenance_work_mem = '1MB';
=> SELECT pg_reload_conf();
```

Запустим очистку и, пока она работает, обратимся несколько раз к представлению `pg_stat_progress_vacuum`:

```text {label="psql"}
=> VACUUM VERBOSE vac;
```

```sql
SELECT * FROM pg_stat_progress_vacuum \gx
-[ RECORD 1 ]--------+--------------
pid                  | 20941
datid                | 16391
datname              | internals
relid                | 16486
phase                | scanning heap
heap_blks_total      | 98483
heap_blks_scanned    | 21329
heap_blks_vacuumed   | 0
index_vacuum_count   | 0
max_dead_tuple_bytes | 67108864
dead_tuple_bytes     | 0
num_dead_item_ids    | 0
indexes_total        | 0
indexes_processed    | 0
delay_time           | 0
```

```sql
SELECT * FROM pg_stat_progress_vacuum \gx
-[ RECORD 1 ]--------+------------------
pid                  | 17602
datid                | 16391
datname              | internals
relid                | 16486
phase                | vacuuming indexes
heap_blks_total      | 172414
heap_blks_scanned    | 36833
heap_blks_vacuumed   | 0
index_vacuum_count   | 0
max_dead_tuple_bytes | 1048576
dead_tuple_bytes     | 1114112
num_dead_item_ids    | 2136372
indexes_total        | 1
indexes_processed    | 0
delay_time           | 0
```

<span id="internal-ru-page-167" class="pdf-anchor"></span>

Представление, в частности, показывает: (v. 17)

- `phase` — текущий этап очистки (выше я описал основные, но вообще их больше [^internal-ru-167-1]);

- `heap_blks_total` — общее число страниц таблицы;

- `heap_blks_scanned` — число просканированных страниц;

- `heap_blks_vacuumed` — число уже очищенных страниц;

- `index_vacuum_count` — количество проходов по индексам;

- `dead_tuple_bytes` и `num_dead_item_ids` — объем и количество мертвых версий, собранных за данный проход.

Общий прогресс очистки определяется отношением `heap_blks_vacuumed` к `heap_blks_total`, но нужно учитывать, что из-за сканирования индексов это значение изменяется «рывками».

Основное внимание стоит обратить на количество циклов очистки (поле `index_vacuum_count`): значение больше единицы сигнализирует о том, что выделенной памяти не хватило для завершения очистки за один проход по индексам. Впрочем, вероятность этого при разумном значении *maintenance\_work\_mem* мала. Эффективность хранения можно оценить по отношению `dead_tuple_bytes` к `num_dead_item_ids`: в данном примере на один `tid` расходуется примерно полбайта.

<span id="internal-ru-dummy-69" class="pdf-anchor"></span>

Если включить параметр *track\_cost\_delay\_timing*, поле `delay_time` покажет фактическое время сна. Надо иметь в виду, что на некоторых платформах отслеживание времени может существенно замедлить выполнение, так что этой возможностью следует пользоваться с осторожностью. (*по умолчанию:* `off`) (v. 18)

Вывод завершившейся к этому времени команды `VACUUM VERBOSE` показывает общую картину:

<span id="internal-ru-page-168" class="pdf-anchor"></span>

```sql {hl_lines="2 21"}
INFO:  vacuuming "internals.public.vac"
INFO:  finished vacuuming "internals.public.vac": index scans: 3
pages: 0 removed, 172414 remain, 172414 scanned (100.00% of total), 0
eagerly scanned
tuples: 5000000 removed, 5000000 remain, 0 are dead but not yet
removable
removable cutoff: 886, which was 0 XIDs old when operation ended
new relfrozenxid: 886, which is 3 XIDs ahead of previous value
frozen: 86208 pages from table (50.00% of total) had 5000000 tuples
frozen
visibility map: 172414 pages set all-visible, 172414 pages set
all-frozen (0 were all-visible)
index scan needed: 86207 pages from table (50.00% of total) had 5000000
dead item identifiers removed
index "vac_s": pages: 8652 in total, 4325 newly deleted, 4325 currently
deleted, 0 reusable
avg read rate: 342.318 MB/s, avg write rate: 341.067 MB/s
buffer usage: 223204 hits, 263773 reads, 262809 dirtied
WAL usage: 591745 records, 177971 full page images, 1300060561 bytes,
14908 buffers full
system usage: CPU: user: 1.03 s, system: 0.76 s, elapsed: 6.01 s
VACUUM
```

В данном случае очистке потребовалось выполнить целых три прохода по индексам.

<span id="internal-ru-section-star-127" class="pdf-anchor"></span>

### Автоочистка {#internal-ru-subsection-star-128}

Регулирование нагрузки для автоматической очистки [^internal-ru-168-1] работает примерно так же, как и для обычной. Но чтобы они могли работать с разной интенсивностью, для автоочистки предусмотрены собственные параметры:

- *autovacuum\_vacuum\_cost\_limit*; (*по умолчанию:* `-1`)

- *autovacuum\_vacuum\_cost\_delay*. (*по умолчанию:* `2ms`)

Если какой-либо из этих параметров равен −1, для него используется значение соответствующего параметра обычной очистки. То есть по умолчанию для *autovacuum\_vacuum\_cost\_limit* применяется значение *vacuum\_cost\_limit*.

<span id="internal-ru-page-169" class="pdf-anchor"></span>

Единицы работы автоочистки, ограниченные в одном цикле пределом *autovacuum\_vacuum\_cost\_limit*, расходуются всеми рабочими процессами, так что общая нагрузка на систему остается примерно одинаковой при любом их количестве. Поэтому если стоит задача ускорить автоочистку, при увеличении параметра *autovacuum\_max\_workers* стоит пропорционально увеличить и значение *autovacuum\_vacuum\_cost\_limit*.

При необходимости для разных таблиц можно устанавливать собственные значения с помощью параметров хранения (*autovacuum\_vacuum\_cost\_limit* и *autovacuum\_vacuum\_cost\_delay*) и их toast-версий.

Представления `need_vacuum` и `need_analyze`, которые мы уже рассмотрели, позволяют отслеживать список таблиц, требующих очистки и анализа. Увеличение длины списка говорит о том, что автоочистка не справляется и ее надо ускорить, уменьшив паузу (*autovacuum\_vacuum\_cost\_delay*) или увеличив объем работы между паузами (*autovacuum\_vacuum\_cost\_limit*). Не исключено, что понадобится также увеличить степень параллелизма (*autovacuum\_max\_workers*).

Помимо запросов есть и штатный способ мониторинга автоочистки — вывод информации (аналогичной той, которую показывает команда `VACUUM VERBOSE`) в журнал сообщений сервера для дальнейшего анализа.

Установим нулевое значение параметра *log\_autovacuum\_min\_duration*, при котором в журнал попадают все запуски автоочистки: (*по умолчанию:* `-1`)

```text {label="psql"}
=> ALTER SYSTEM SET log_autovacuum_min_duration = 0;
```

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

```text {label="psql"}
=> ALTER SYSTEM SET track_cost_delay_timing = on;
=> SELECT pg_reload_conf();
```

Снова обновим все строки таблицы, чтобы автоочистке было чем заняться:

<span id="internal-ru-page-170" class="pdf-anchor"></span>

```text {label="psql" hl_lines="4 20 25 26"}
=> UPDATE vac SET s = 'C';
UPDATE 5000000
postgres$ tail -n 20 /home/postgres/logfile
2026-04-05 20:54:21.347 MSK [51046] LOG:  automatic vacuum of table
"internals.public.vac": index scans: 3
    pages: 0 removed, 172414 remain, 172414 scanned (100.00% of total),
0 eagerly scanned
    tuples: 5000000 removed, 5000000 remain, 0 are dead but not yet
removable
    removable cutoff: 888, which was 0 XIDs old when operation ended
    new relfrozenxid: 888, which is 2 XIDs ahead of previous value
    frozen: 86208 pages from table (50.00% of total) had 5000000 tuples
frozen
    visibility map: 172414 pages set all-visible, 172414 pages set
all-frozen (0 were all-visible)
    index scan needed: 86208 pages from table (50.00% of total) had
5000000 dead item identifiers removed
    index "vac_s": pages: 16923 in total, 4325 newly deleted, 8650
currently deleted, 4325 reusable
    delay time: 68789.809 ms
    avg read rate: 26.782 MB/s, avg write rate: 24.155 MB/s
    buffer usage: 236780 hits, 288039 reads, 259792 dirtied
    WAL usage: 616268 records, 187331 full page images, 1502256915
bytes, 9413 buffers full
    system usage: CPU: user: 5.13 s, system: 4.50 s, elapsed: 84.02 s
2026-04-05 20:54:22.349 MSK [51046] LOG:  automatic analyze of table
"internals.public.vac"
    delay time: 663.495 ms
    avg read rate: 214.227 MB/s, avg write rate: 0.008 MB/s
    buffer usage: 2742 hits, 27421 reads, 1 dirtied
    WAL usage: 5 records, 1 full page images, 5509 bytes, 0 buffers full
    system usage: CPU: user: 0.20 s, system: 0.06 s, elapsed: 0.99 s
```

Из журнала следует, что бо́льшую часть времени автоочистка простаивала, так что для увеличения скорости имеется достаточный резерв.

Общее время, затраченное на очистку и анализ таблицы (в миллисекундах), можно узнать из системной статистики: (v. 18)

```text {label="psql"}
=> SELECT total_vacuum_time, total_autovacuum_time,
  total_analyze_time, total_autoanalyze_time
FROM pg_stat_all_tables WHERE relname = 'vac' \gx
-[ RECORD 1 ]----------+------
total_vacuum_time      | 6025
total_autovacuum_time  | 84026
total_analyze_time     | 0
total_autoanalyze_time | 1825
```

[^internal-ru-139-1]: [postgrespro.ru/docs/postgresql/18/routine-vacuuming#AUTOVACUUM](<https://postgrespro.ru/docs/postgresql/18/routine-vacuuming#AUTOVACUUM>).
[^internal-ru-140-1]: [backend/postmaster/autovacuum.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/autovacuum.c;hb=REL_18_STABLE>).
[^internal-ru-145-1]: [backend/postmaster/autovacuum.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/autovacuum.c;hb=REL_18_STABLE>), функция relation\_needs\_vacanalyze.
[^internal-ru-153-1]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), функция heap\_vac\_scan\_next\_block.
[^internal-ru-156-1]: [commitfest.postgresql.org/37/3433](<https://commitfest.postgresql.org/37/3433>).
[^internal-ru-157-1]: [backend/access/transam/varsup.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/varsup.c;hb=REL_18_STABLE>), функция SetTransactionIdLimit.
[^internal-ru-158-1]: [backend/commands/vacuum.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/commands/vacuum.c;hb=REL_18_STABLE>), функция vac\_truncate\_clog.
[^internal-ru-158-2]: [backend/access/heap/vacuumlazy.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/vacuumlazy.c;hb=REL_18_STABLE>), функция lazy\_check\_wraparound\_failsafe.
[^internal-ru-165-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция ReadBuffer\_common.
[^internal-ru-165-2]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция MarkBufferDirty.
[^internal-ru-167-1]: [postgrespro.ru/docs/postgresql/18/progress-reporting#VACUUM-PHASES](<https://postgrespro.ru/docs/postgresql/18/progress-reporting#VACUUM-PHASES>).
[^internal-ru-168-1]: [backend/postmaster/autovacuum.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/postmaster/autovacuum.c;hb=REL_18_STABLE>), функция autovac\_balance\_cost.
