# Занятие 07: Многоверсионность / Заморозка

> Исходная страница в нативном Markdown: Многоверсионность / Заморозка

---

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

---

<!-- Generated by scripts/generate_skeleton.py. -->

> [!NOTE]
> Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета.
> **HTML source:** [dba2_07_mvcc_freeze.html](https://edu.postgrespro.ru/16/dba2-16/dba2_07_mvcc_freeze.html)
> **Archive:** [DBA2-handouts-16.zip](https://edu.postgrespro.ru/16/DBA2-handouts-16.zip)
> **Страниц PDF:** 24

## Page 1: Многоверсионность / Заморозка {#source-page-001}

### Авторские права

© Postgres Professional, 2016–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
### Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
### Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
### Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

## Page 2: Темы {#source-page-002}

- Проблема переполнения счетчика транзакций
- Заморозка версий строк и правила видимости
- Настройка автоочистки для выполнения заморозки
- Заморозка вручную

## Page 3: Переполнение счетчика {#source-page-003}

меньшие номера — прошлое, бóльшие — будущее разрядность счетчика — 32 бита, что делать при переполнении?
  - транзакция 2
  - началась позже
  - транзакции 1
  - 1
  - 2
  - прошлое
  - будущее

### Комментарии преподавателя

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

Под номер транзакции в PostgreSQL выделено 32 бита. Это довольно большое число (около 4 млрд номеров), но при активной работе сервера оно вполне может быть исчерпано. Например при нагрузке 1000 транзакций в секунду это произойдет всего через полтора месяца непрерывной работы.

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

Почему под номер транзакции не выделено 64 бита — ведь это полностью исключило бы проблему? Дело в том, что (как рассматривалось в теме «Страницы и версии строк») в заголовке каждой версии строки хранятся два номера транзакций — xmin и xmax. Заголовок и так достаточно большой, а увеличение разрядности привело бы к его увеличению еще на 8 байт.

## Page 4: Нумерация по кругу {#source-page-004}

![Source slide page 4: Нумерация по кругу](images/page-004.png){#source-figure-004 caption="Source slide page 4: Нумерация по кругу"}

пространство номеров транзакций закольцовано половина номеров — прошлое, половина — будущее

### Комментарии преподавателя

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

Возрастом транзакции называется число транзакций, прошедших с момента ее появления в системе (независимо от того, переходил ли счетчик через ноль или нет).

## Page 5: Проблема видимости {#source-page-005}

![Source slide page 5: Проблема видимости](images/page-005.png){#source-figure-005 caption="Source slide page 5: Проблема видимости"}


### Комментарии преподавателя

В такой закольцованной схеме возникает неприятная ситуация. Транзакция, находившаяся в далеком прошлом (транзакция 1 на слайде), через некоторое время окажется в той половине круга, которая относится к будущему. Это, конечно, нарушит правила видимости и приведет к проблемам.

## Page 6: Заморозка версий строк {#source-page-006}

![Source slide page 6: Заморозка версий строк](images/page-006.png){#source-figure-006 caption="Source slide page 6: Заморозка версий строк"}

замороженные версии строк считаются «бесконечно старыми» номер транзакции xmin может быть использован заново

### Комментарии преподавателя

Чтобы не допустить путешествий из прошлого в будущее, процесс очистки выполняет еще одну задачу. Он находит достаточно старые и «холодные» версии строк (которые видны во всех снимках и изменение которых уже маловероятно) и специальным образом помечает — «замораживает» — их. Замороженная версия строки считается старше любых обычных данных и всегда видна во всех снимках данных. При этом уже не требуется смотреть на номер транзакции xmin, и этот номер может быть безопасно использован заново. Таким образом, замороженные версии строк всегда остаются в прошлом.

<https://postgrespro.ru/docs/postgresql/16/routine-vacuuming#VACUUM-FOR> -WRAPAROUND

## Page 7: Заморозка версий строк {#source-page-007}

![Source slide page 7: Заморозка версий строк](images/page-007.png){#source-figure-007 caption="Source slide page 7: Заморозка версий строк"}

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

### Комментарии преподавателя

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

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

Многие источники (включая документацию) упоминают специальный номер FrozenTransactionId = 2, который записывается на место xmin в замороженных версиях. Такая система действовала до версии 9.4, но сейчас заменена на биты-подсказки — это позволяет сохранить в версии строки исходный номер транзакции, что удобно для целей поддержки и отладки. Однако транзакции с номером 2 еще могут встретиться в старых системах, даже обновленных до последних версий.

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

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

## Page 8: Настройка {#source-page-008}

![Source slide page 8: Настройка](images/page-008.png){#source-figure-008 caption="Source slide page 8: Настройка"}

**vacuum\\\_freeze\\\_min\\\_age**
- минимальный возраст, с которого начинается заморозка

### Комментарии преподавателя

Заморозкой управляют четыре основных параметра.

Параметр vacuum\_freeze\_min\_age определяет минимальный возраст транзакции xmin, с которого начинается заморозка.

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

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

Заметим, что очистка просматривает только страницы, не отмеченные в карте видимости. Если на странице остались только актуальные версии, то очистка не придет в такую страницу и не заморозит их.

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

## Page 9: Настройка {#source-page-009}

![Source slide page 9: Настройка](images/page-009.png){#source-figure-009 caption="Source slide page 9: Настройка"}

**vacuum\\\_freeze\\\_table\\\_age**
- при достижении такого возраста замораживаются версии строк на всех страницах («агрессивная» заморозка) для ускорения используется карта заморозки

### Комментарии преподавателя

Параметр vacuum\_freeze\_table\_age определяет возраст транзакции, при котором пора выполнять заморозку версий строк на всех страницах таблицы. Такая заморозка называется «агрессивной».

Для каждой таблицы хранится номер транзакции (pg\_class.relfrozenxid), для которого известно, что в версиях строк не осталось более старых незамороженных номеров транзакций. Возраст этой транзакции и сравнивается со значением параметра.

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

Даже в агрессивном режиме все версии строк с транзакциями младше vacuum\_freeze\_min\_age не замораживаются, поэтому после заморозки новый возраст транзакции relfrozenxid будет равен не нулю, а vacuum\_freeze\_min\_age. Таким образом, заморозка всех страниц выполняется раз в (vacuum\_freeze\_table\_age − vacuum\_freeze\_min\_age) транзакций.

Мы уже говорили, что слишком маленькое значение параметра vacuum\_freeze\_min\_age увеличивает накладные расходы на очистку. Но при больших значениях агрессивная заморозка будет выполняться слишком часто, что тоже плохо. Установка этого параметра требует компромисса.

## Page 10: Настройка {#source-page-010}

![Source slide page 10: Настройка](images/page-010.png){#source-figure-010 caption="Source slide page 10: Настройка"}

**autovacuum\\\_freeze\\\_max\\\_age**
- при достижении такого возраста заморозка запускается принудительно определяет размер CLOG
- VACUUM (index\_cleanup off)

### Комментарии преподавателя

Параметр autovacuum\_freeze\_max\_age определяет возраст транзакции, при котором заморозка будет выполняться принудительно. Автоочистка для предотвращения последствий переполнения счетчика транзакций запустится, даже если она отключена параметрами.

Этот параметр также определяет размер структуры CLOG: данные о статусе более старых транзакций точно никогда не понадобятся, поэтому часть файлов из PGDATA/pg\_xact может быть удалена.

Если администратор понимает, что автоочистка не успеет заморозить версии строк до переполнения счетчика транзакций, можно воспользоваться ручной очисткой с параметром index\_cleanup off. В этом случае индексы не будут очищаться, но за счет этого версии строк в таблицах будут заморожены быстрее.

## Page 11: Настройка {#source-page-011}

![Source slide page 11: Настройка](images/page-011.png){#source-figure-011 caption="Source slide page 11: Настройка"}

**vacuum\\\_failsafe\\\_age**
- при достижении такого возраста очистка переходит в защитный режим работы

### Комментарии преподавателя

Параметр vacuum\_failsafe\_age управляет включением защитного режима работы очистки, который служит для ускорения заморозки номеров транзакций.

В этом режиме задержки, задаваемые параметрами autovacuum\_vacuum\_cost\_delay и vacuum\_cost\_delay, будут отменены. Также не будут выполняться некоторые необязательные работы (например очистка индексов). Такие меры позволят очистке быстрее заморозить старые транзакции и перейти в обычный режим работы.

## Page 12: Настройка {#source-page-012}

**Конфигурационные параметры**
  - vacuum\_freeze\_min\_age
  - = 0 50 000 000
  - vacuum\_freeze\_table\_age
  - = 150 000 000
  - autovacuum\_freeze\_max\_age = 200 000 000
  - vacuum\_failsafe\_age
  - = 1 600 000 000
  - ↑?
- Параметры хранения таблиц
  - autovacuum\_freeze\_min\_age
  - toast.autovacuum\_freeze\_min\_age
  - autovacuum\_freeze\_table\_age
  - toast.autovacuum\_freeze\_table\_age
  - autovacuum\_freeze\_max\_age
  - toast.autovacuum\_freeze\_max\_age

### Комментарии преподавателя

Значения по умолчанию довольно консервативны. Предел для autovacuum\_freeze\_max\_age – порядка 2 млрд транзакций, а используется значение в 10 раз меньшее. Можно увеличить значения vacuum\_freeze\_table\_age и autovacuum\_freeze\_max\_age для уменьшения накладных расходов, но важно понимать, что если по каким-то причинам (например, из-за активной транзакции) автоочистка вовремя не справится с заморозкой, у администратора останется мало времени для принятия мер. Заметьте, что изменение параметра autovacuum\_freeze\_max\_age требует перезапуска сервера.

Значение по умолчанию vacuum\_failsafe\_age значительно больше, чем autovacuum\_freeze\_max\_age, и если до исчерпания номеров останется мало времени, защитный режим ускорит заморозку.

Отметим, что сервер может скорректировать установленные значения параметров vacuum\_freeze\_min\_age, vacuum\_freeze\_table\_age и vacuum\_failsafe\_age исходя из значения autovacuum\_freeze\_max\_age.

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

В модуле «Блокировки» рассматриваются т. н. мультитранзакции и дополнительные параметры настройки заморозки для них.

<https://postgrespro.ru/docs/postgresql/16/runtime-config-client>

<https://postgrespro.ru/docs/postgresql/16/runtime-config-autovacuum>

## Page 13: Заморозка {#source-page-013}

Установим для демонстрации параметры заморозки.
Небольшой возраст транзакции:
```sql
=> ALTER SYSTEM SET vacuum_freeze_min_age = 1;
```
```sql
ALTER SYSTEM
```
Возраст, после которого будет выполняться заморозка всех страниц:
```sql
=> ALTER SYSTEM SET vacuum_freeze_table_age = 3;
```
```sql
ALTER SYSTEM
```
И отключим автоматическую очистку, чтобы запускать ее вручную в нужный момент.
```sql
=> ALTER SYSTEM SET autovacuum = off;
```
```sql
ALTER SYSTEM
```
```sql
=> SELECT pg_reload_conf();
```
```text
 pg_reload_conf
----------------
 t
(1 row)
```
Создадим таблицу с данными. Установим минимальный fillfactor: на каждой странице будет всего две строки.
```sql
=> CREATE DATABASE mvcc_freeze;
```
```sql
CREATE DATABASE
```
```sql
=> \c mvcc_freeze
```
```text
You are now connected to database "mvcc_freeze" as user "student".
```
```sql
=> CREATE TABLE t(id integer, s char(300)) WITH (fillfactor = 10);
```
```sql
CREATE TABLE
```
Создадим представление для наблюдения за битами-подсказками на первых двух страницах таблицы.
Сейчас нас интересует только xmin и биты, которые относятся к нему, поскольку версии строк с ненулевым xmax будут очищены. Кроме того, выведем и возраст транзакции xmin.
```sql
=> CREATE EXTENSION pageinspect;
```
```sql
CREATE EXTENSION
```
```sql
=> CREATE VIEW t_v AS
SELECT '('||blkno||','||lp||')' 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 AS xmin,
       age(t_xmin) AS xmin_age,
       CASE WHEN (t_infomask & 256) > 0 THEN 't' END AS xmin_c,
       CASE WHEN (t_infomask & 512) > 0 THEN 't' END AS xmin_a,
       t_xmax AS xmax
FROM (
  SELECT 0 blkno, * FROM heap_page_items(get_raw_page('t',0))
  UNION ALL
  SELECT 1 blkno, * FROM heap_page_items(get_raw_page('t',1))
) q
ORDER BY blkno, lp;
```
```sql
CREATE VIEW
```
Для того чтобы заглянуть в карту видимости и заморозки, воспользуемся еще одним расширением:
```sql
=> CREATE EXTENSION pg_visibility;
```
```sql
CREATE EXTENSION
```

## Page 14: Page 14 {#source-page-014}

Вставляем данные. Сразу выполним очистку, чтобы построить карту видимости.
```sql
=> INSERT INTO t(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id);
```
```sql
INSERT 0 100
```
```sql
=> VACUUM t;
```
```sql
VACUUM
```
После очистки обе страницы отмечены в карте видимости (all\_visible):
```sql
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
```
```text
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | f
     1 | t           | f
(2 rows)
```
Каков возраст транзакции, создавшей строки?
```sql
=> SELECT * FROM t_v;
```
```text
 ctid  | state  | xmin | xmin_age | xmin_c | xmin_a | xmax
-------+--------+------+----------+--------+--------+------
 (0,1) | normal |  748 |        1 | t      |        |    0
 (0,2) | normal |  748 |        1 | t      |        |    0
 (1,1) | normal |  748 |        1 | t      |        |    0
 (1,2) | normal |  748 |        1 | t      |        |    0
(4 rows)
```
Возраст равен 1; версии строк с такой транзакцией еще не будут заморожены.
Обновим строку на нулевой странице. Новая версия попадет на ту же страницу благодаря небольшому значению fillfactor.
```sql
=> UPDATE t SET s = 'BAR' WHERE id = 1;
```
```sql
UPDATE 1
```
```sql
=> SELECT * FROM t_v;
```
```text
 ctid  | state  | xmin | xmin_age | xmin_c | xmin_a | xmax
-------+--------+------+----------+--------+--------+------
 (0,1) | normal |  748 |        2 | t      |        |  749
 (0,2) | normal |  748 |        2 | t      |        |    0
 (0,3) | normal |  749 |        1 |        |        |    0
 (1,1) | normal |  748 |        2 | t      |        |    0
 (1,2) | normal |  748 |        2 | t      |        |    0
(5 rows)
```
Сейчас нулевая страница уже будет обработана заморозкой:
возраст транзакции превышает значение, установленное в vacuum\_freeze\_min\_age; страница изменена и исключена из карты видимости.
```sql
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
```
```text
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | f           | f
     1 | t           | f
(2 rows)
```
Выполняем очистку.
```sql
=> VACUUM t;
```
```sql
VACUUM
```
Очистка обработала измененную страницу. У одной версии строки установлены оба бита — это признак заморозки. Другая версия строки слишком молода, однако тоже была заморожена при проходе страницы (это позволило отметить нулевую страницу в карте заморозки):
```sql
=> SELECT * FROM t_v;
```

## Page 15: Page 15 {#source-page-015}

```text
 ctid  |     state     | xmin | xmin_age | xmin_c | xmin_a | xmax
-------+---------------+------+----------+--------+--------+------
 (0,1) | redirect to 3 |      |          |        |        |
 (0,2) | normal        |  748 |        2 | t      | t      |    0
 (0,3) | normal        |  749 |        1 | t      | t      |    0
 (1,1) | normal        |  748 |        2 | t      |        |    0
 (1,2) | normal        |  748 |        2 | t      |        |    0
(5 rows)
```
Теперь обе страницы отмечены в карте видимости (все версии строк на них актуальны). Очистка теперь не будет обрабатывать ни одну из этих страниц, и незамороженные версии строк на первой странице так и останутся незамороженными.
```sql
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
```
```text
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | t
     1 | t           | f
(2 rows)
```
Именно для такого случая и требуется параметр vacuum\_freeze\_table\_age, определяющий, в какой момент нужно просмотреть страницы, отмеченные в карте видимости, если они не отмечены в карте заморозки.
Для каждой таблицы сохраняется наибольший номер транзакции, для которого все версии строк с меньшими номерами xmin гарантированно заморожены. Ее возраст и сравнивается со значением параметра.
```sql
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
```
```text
 relfrozenxid | age
--------------+-----
          748 |   2
(1 row)
```
Сымитируем выполнение еще одной транзакции, чтобы возраст relfrozenxid таблицы достиг значения параметра vacuum\_freeze\_table\_age.
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                750
(1 row)
```
```sql
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
```
```text
 relfrozenxid | age
--------------+-----
          748 |   3
(1 row)
```
```sql
=> VACUUM t;
```
```sql
VACUUM
```
Теперь, поскольку гарантированно была проверена вся таблица, номер замороженной транзакции можно увеличить — мы уверены, что в страницах не осталось более старой незамороженной транзакции.
```sql
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
```
```text
 relfrozenxid | age
--------------+-----
          751 |   0
(1 row)
```
Вот что получилось в страницах:
```sql
=> SELECT * FROM t_v;
```

## Page 16: Page 16 {#source-page-016}

```text
 ctid  |     state     | xmin | xmin_age | xmin_c | xmin_a | xmax
-------+---------------+------+----------+--------+--------+------
 (0,1) | redirect to 3 |      |          |        |        |
 (0,2) | normal        |  748 |        3 | t      | t      |    0
 (0,3) | normal        |  749 |        2 | t      | t      |    0
 (1,1) | normal        |  748 |        3 | t      | t      |    0
 (1,2) | normal        |  748 |        3 | t      | t      |    0
(5 rows)
```
Обе страницы теперь отмечены в карте заморозки.
```sql
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
```
```text
 blkno | all_visible | all_frozen
-------+-------------+------------
     0 | t           | t
     1 | t           | t
(2 rows)
```
Номер последней замороженной транзакции есть и на уровне всей БД:
```sql
=> SELECT datname, datfrozenxid, age(datfrozenxid)
FROM pg_database;
```
```text
   datname   | datfrozenxid | age
-------------+--------------+-----
 postgres    |          722 |  29
 student     |          722 |  29
 template1   |          722 |  29
 template0   |          722 |  29
 mvcc_freeze |          722 |  29
(5 rows)
```
Он устанавливается в минимальное значение из relfrozenxid всех таблиц этой БД. Если возраст datfrozenxid превысит значение параметра autovacuum\_freeze\_max\_age, автоочистка будет запущена принудительно.

## Page 17: Заморозка вручную {#source-page-017}

VACUUM
  - заморозка версий строк по возрасту в соответствии с настройками
- VACUUM FREEZE
  - принудительная заморозка версий строк с xmin любого возраста
  - тот же эффект и при VACUUM FULL, CLUSTER
- COPY ... WITH FREEZE
  - принудительная заморозка сразу после загрузки
  - таблица должна быть создана или опустошена в той же транзакции
  - могут нарушиться правила изоляции транзакции

### Комментарии преподавателя

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

Команда VACUUM, как и автоочистка, выполнит заморозку в соответствии с настройками.

Если выполнить команду VACUUM FREEZE, будут заморожены все версии строк без оглядки на возраст транзакций (как будто параметры vacuum\_freeze\_min\_age и vacuum\_freeze\_table\_age равны нулю). При перестройке таблицы командами VACUUM FULL или CLUSTER все строки также замораживаются.

<https://postgrespro.ru/docs/postgresql/16/sql-vacuum>

Данные можно заморозить и при начальной загрузке с помощью команды COPY, указав параметр FREEZE. Для этого таблица должна быть создана (или опустошена командой TRUNCATE) в той же транзакции, что и COPY. Поскольку для замороженных строк действуют отдельные правила видимости, такие строки будут видны в снимках данных других транзакций в нарушение обычных правил изоляции (для транзакций с уровнем Repeatable Read или Serializable), но обычно это не представляет проблемы. Подробнее такой случай рассматривается в практике.

<https://postgrespro.ru/docs/postgresql/16/sql-copy>

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

## Page 18: Итоги {#source-page-018}

**Пространство номеров транзакций закольцовано**
- Достаточно старые версии строк замораживаются процессом очистки
- Для оптимизации используется карта заморозки

## Page 19: Практика {#source-page-019}

1. Проверьте с помощью расширения pageinspect, что при использовании команды COPY ... WITH FREEZE версии строк действительно замораживаются.
- 2. Убедитесь, что даже на уровне изоляции Repeatable Read строки, загруженные командой COPY ... WITH FREEZE, оказываются видны в снимке данных.
- 3. Уменьшив значение параметра autovacuum\_freeze\_max\_age и отключив автоочистку, воспроизведите ситуацию принудительного срабатывания автоочистки, выполнив соответствующее количество транзакций. Учтите, что срабатывание произойдет не сразу, а при выполнении ручной очистки какой-нибудь таблицы (или при перезапуске сервера).

### Комментарии преподавателя

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

Можно организовать цикл в bash, в котором вызывать psql с командой обновления: psql -c 'UPDATE ...'

Другой вариант — использовать для организации цикла PL/pgSQL. Для этого можно создать процедуру, выполняющую фиксацию транзакции или использовать блок с обработкой исключений. При перехвате исключения транзакция будет откатываться к неявной точке сохранения: фактически начнется новая вложенная транзакция (см. тему «Страницы и версии строк»).

Третий вариант — использовать утилиту pgbench:

<https://postgrespro.ru/docs/postgresql/16/pgbench>

## Page 20: 1. Заморозка при COPY WITH FREEZE {#source-page-020}

Создаем таблицу и загружаем несколько строк в одной и той же транзакции:
```sql
=> CREATE DATABASE mvcc_freeze;
```
```sql
CREATE DATABASE
```
```sql
=> \c mvcc_freeze
```
```text
You are now connected to database "mvcc_freeze" as user "student".
```
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> CREATE TABLE t(n integer);
```
```sql
CREATE TABLE
```
```sql
=> COPY t FROM stdin WITH FREEZE;
```
```text
=> 1
```
```text
=> 2
```
```text
=> 3
```
```text
=> \.
```
```text
COPY 3
```
```text
=> COMMIT;
```
```text
COMMIT
```
Проверяем версии строк:
```sql
=> CREATE EXTENSION pageinspect;
```
```sql
CREATE EXTENSION
```
```sql
=> CREATE VIEW t_v AS
SELECT '(0,'||lp||')' 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 AS xmin,
       age(t_xmin) AS xmin_age,
       CASE WHEN (t_infomask & 256) > 0 THEN 't' END AS xmin_c,
       CASE WHEN (t_infomask & 512) > 0 THEN 't' END AS xmin_a,
       t_xmax AS xmax,
       t_ctid
FROM heap_page_items(get_raw_page('t',0))
ORDER BY lp;
```
```sql
CREATE VIEW
```
```sql
=> SELECT * FROM t_v;
```
```text
 ctid  | state  | xmin | xmin_age | xmin_c | xmin_a | xmax | t_ctid
-------+--------+------+----------+--------+--------+------+--------
 (0,1) | normal |  744 |        3 | t      | t      |    0 | (0,1)
 (0,2) | normal |  744 |        3 | t      | t      |    0 | (0,2)
 (0,3) | normal |  744 |        3 | t      | t      |    0 | (0,3)
(3 rows)
```
### 2. COPY WITH FREEZE и изоляция
В другом сеансе начнем транзакцию с уровнем изоляции Repeatable Read.
```sql
=> \c mvcc_freeze
```
```text
You are now connected to database "mvcc_freeze" as user "student".
```
```text
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
```

## Page 21: Page 21 {#source-page-021}

```text
BEGIN
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                747
(1 row)
```
Обратите внимание, что эта транзакция не должна обращаться к таблице t.
Теперь опустошим таблицу и загрузим в нее новые строки в одной транзакции. Если бы параллельная транзакция прочитала содержимое t, команда TRUNCATE ожидала бы ее завершения.
```text
=> BEGIN;
```
```text
BEGIN
```
```text
=> TRUNCATE t;
```
```text
TRUNCATE TABLE
```
```sql
=> COPY t FROM stdin WITH FREEZE;
```
```text
=> 10
```
```text
=> 20
```
```text
=> 30
```
```text
=> \.
```
```text
COPY 3
```
```text
=> COMMIT;
```
```text
COMMIT
```
Теперь параллельная транзакция видит новые данные, хотя это и нарушает изоляцию:
```sql
=> SELECT * FROM t;
```
```text
 n
----
 10
 20
 30
(3 rows)
```
```text
=> COMMIT;
```
```text
COMMIT
```
```sql
=> \q
```
### 3. Аварийное срабатывание автоочистки
Предварительно заморозим все транзакции во всех базах. Для этого удобно воспользоваться командой vacuumdb:
```sh
student$ vacuumdb --all --freeze
```
```text
vacuumdb: vacuuming database "mvcc_freeze"
vacuumdb: vacuuming database "postgres"
vacuumdb: vacuuming database "student"
vacuumdb: vacuuming database "template1"
```
Максимальный возраст незамороженных транзакций по всем БД:
```sql
=> SELECT datname, datfrozenxid, age(datfrozenxid) FROM pg_database;
```
```text
   datname   | datfrozenxid | age
-------------+--------------+-----
 postgres    |          749 |   0
 student     |          749 |   0
 template1   |          749 |   0
 template0   |          722 |  27
 mvcc_freeze |          749 |   0
(5 rows)
```
Отключаем автоочистку.
```sql
=> ALTER SYSTEM SET autovacuum = off;
```

## Page 22: Page 22 {#source-page-022}

```sql
ALTER SYSTEM
```
Уменьшаем значения параметров:
```sql
=> ALTER SYSTEM SET vacuum_freeze_min_age = 1_000;
```
```sql
ALTER SYSTEM
```
```sql
=> ALTER SYSTEM SET vacuum_freeze_table_age = 10_000;
```
```sql
ALTER SYSTEM
```
```sql
=> ALTER SYSTEM SET autovacuum_freeze_max_age = 100_000; # минимальное значение
```
```sql
ALTER SYSTEM
```
Требуется перезагрузка сервера.
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql mvcc_freeze
```
Получить большое количество транзакций можно разными способами; например, можно воспользоваться утилитой pgbench. Попросим ее инициализировать свои таблицы и выполнить 100000 транзакций.
```sh
student$ pgbench -i mvcc_freeze
```
```sql
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.15 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.47 s (drop tables 0.00 s, create tables 0.03 s, client-side generate 0.18 s,
vacuum 0.10 s, primary keys 0.15 s).
```
Выполнение ста тысяч транзакций может занять заметное время, ключ --protocol=prepared немного ускоряет работу за счет использования подготовленных операторов:
```sh
student$ pgbench -t 100000 -P 15 --protocol=prepared mvcc_freeze
```
```sql
pgbench (16.2 (Ubuntu 16.2-2.pgdg24.04+1))
starting vacuum...end.
progress: 15.0 s, 109.9 tps, lat 9.090 ms stddev 1.740, 0 failed
progress: 30.0 s, 87.1 tps, lat 11.472 ms stddev 4.723, 0 failed
progress: 45.0 s, 115.6 tps, lat 8.648 ms stddev 1.734, 0 failed
progress: 60.0 s, 121.3 tps, lat 8.245 ms stddev 1.168, 0 failed
progress: 75.0 s, 120.7 tps, lat 8.285 ms stddev 1.105, 0 failed
progress: 90.0 s, 122.5 tps, lat 8.163 ms stddev 1.452, 0 failed
progress: 105.0 s, 123.6 tps, lat 8.090 ms stddev 0.956, 0 failed
progress: 120.0 s, 118.0 tps, lat 8.473 ms stddev 1.570, 0 failed
progress: 135.0 s, 117.9 tps, lat 8.481 ms stddev 1.127, 0 failed
progress: 150.0 s, 120.4 tps, lat 8.304 ms stddev 1.432, 0 failed
progress: 165.0 s, 120.5 tps, lat 8.295 ms stddev 1.022, 0 failed
progress: 180.0 s, 121.1 tps, lat 8.261 ms stddev 1.014, 0 failed
progress: 195.0 s, 121.4 tps, lat 8.234 ms stddev 0.986, 0 failed
progress: 210.0 s, 120.8 tps, lat 8.276 ms stddev 1.115, 0 failed
progress: 225.0 s, 123.0 tps, lat 8.131 ms stddev 1.193, 0 failed
progress: 240.0 s, 122.9 tps, lat 8.138 ms stddev 0.985, 0 failed
progress: 255.0 s, 122.9 tps, lat 8.136 ms stddev 0.930, 0 failed
progress: 270.0 s, 109.1 tps, lat 9.169 ms stddev 2.281, 0 failed
progress: 285.0 s, 72.7 tps, lat 13.746 ms stddev 1.832, 0 failed
progress: 300.0 s, 71.7 tps, lat 13.934 ms stddev 2.546, 0 failed
progress: 315.0 s, 71.6 tps, lat 13.968 ms stddev 4.179, 0 failed
progress: 330.0 s, 72.0 tps, lat 13.888 ms stddev 2.334, 0 failed
progress: 345.0 s, 66.4 tps, lat 15.050 ms stddev 3.755, 0 failed
progress: 360.0 s, 70.0 tps, lat 14.293 ms stddev 2.912, 0 failed
progress: 375.0 s, 71.2 tps, lat 14.044 ms stddev 7.088, 0 failed
progress: 390.0 s, 74.3 tps, lat 13.457 ms stddev 3.926, 0 failed
progress: 405.0 s, 87.8 tps, lat 11.390 ms stddev 3.061, 0 failed
progress: 420.0 s, 97.6 tps, lat 10.243 ms stddev 2.956, 0 failed
progress: 435.0 s, 116.1 tps, lat 8.613 ms stddev 1.184, 0 failed
progress: 450.0 s, 118.5 tps, lat 8.442 ms stddev 1.582, 0 failed
progress: 465.0 s, 122.0 tps, lat 8.196 ms stddev 1.217, 0 failed
progress: 480.0 s, 121.1 tps, lat 8.259 ms stddev 1.349, 0 failed
progress: 495.0 s, 123.3 tps, lat 8.112 ms stddev 1.076, 0 failed
progress: 510.0 s, 121.9 tps, lat 8.202 ms stddev 2.393, 0 failed
progress: 525.0 s, 118.7 tps, lat 8.421 ms stddev 2.103, 0 failed
progress: 540.0 s, 121.9 tps, lat 8.201 ms stddev 1.238, 0 failed
```

## Page 23: Page 23 {#source-page-023}

```text
progress: 555.0 s, 123.4 tps, lat 8.101 ms stddev 0.977, 0 failed
progress: 570.0 s, 123.1 tps, lat 8.124 ms stddev 1.088, 0 failed
progress: 585.0 s, 121.7 tps, lat 8.215 ms stddev 1.289, 0 failed
progress: 600.0 s, 121.7 tps, lat 8.217 ms stddev 1.295, 0 failed
progress: 615.0 s, 122.3 tps, lat 8.172 ms stddev 1.191, 0 failed
progress: 630.0 s, 122.2 tps, lat 8.183 ms stddev 1.515, 0 failed
progress: 645.0 s, 121.2 tps, lat 8.252 ms stddev 1.456, 0 failed
progress: 660.0 s, 102.9 tps, lat 9.715 ms stddev 2.726, 0 failed
progress: 675.0 s, 122.3 tps, lat 8.170 ms stddev 1.029, 0 failed
progress: 690.0 s, 120.7 tps, lat 8.282 ms stddev 1.291, 0 failed
progress: 705.0 s, 121.0 tps, lat 8.264 ms stddev 1.314, 0 failed
progress: 720.0 s, 119.5 tps, lat 8.362 ms stddev 1.477, 0 failed
progress: 735.0 s, 123.2 tps, lat 8.117 ms stddev 1.076, 0 failed
progress: 750.0 s, 121.9 tps, lat 8.204 ms stddev 1.254, 0 failed
progress: 765.0 s, 121.8 tps, lat 8.209 ms stddev 1.482, 0 failed
progress: 780.0 s, 120.9 tps, lat 8.270 ms stddev 1.491, 0 failed
progress: 795.0 s, 123.2 tps, lat 8.119 ms stddev 1.102, 0 failed
progress: 810.0 s, 120.8 tps, lat 8.274 ms stddev 1.249, 0 failed
progress: 825.0 s, 120.7 tps, lat 8.284 ms stddev 1.505, 0 failed
progress: 840.0 s, 120.3 tps, lat 8.310 ms stddev 2.076, 0 failed
progress: 855.0 s, 118.9 tps, lat 8.407 ms stddev 1.477, 0 failed
progress: 870.0 s, 122.3 tps, lat 8.178 ms stddev 1.075, 0 failed
progress: 885.0 s, 119.1 tps, lat 8.393 ms stddev 1.288, 0 failed
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: prepared
number of clients: 1
number of threads: 1
maximum number of tries: 1
number of transactions per client: 100000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 8.917 ms
latency stddev = 2.531 ms
initial connection time = 7.228 ms
tps = 112.124179 (without initial connection time)
```
Видно, что возраст незамороженных транзакций превышает установленное пороговое значение (100000):
```sql
=> SELECT datname, datfrozenxid, age(datfrozenxid) FROM pg_database;
```
```text
   datname   | datfrozenxid |  age
-------------+--------------+--------
 postgres    |          749 | 100013
 student     |          749 | 100013
 template1   |          749 | 100013
 template0   |          722 | 100040
 mvcc_freeze |          749 | 100013
(5 rows)
```
Теперь при выполнении команды VACUUM для любой таблицы будет запущен процесс автоочистки.
```sql
=> VACUUM t;
```
```sql
VACUUM
```
Среди процессов появился autovacuum worker:
```sh
student$ sudo head -n 1 /var/lib/postgresql/16/main/postmaster.pid
```
```text
52894
```
```sh
student$ ps -o pid,command --ppid 52894
```
```text
    PID COMMAND
  52895 postgres: 16/main: checkpointer
  52896 postgres: 16/main: background writer
  52898 postgres: 16/main: walwriter
  52899 postgres: 16/main: logical replication launcher
  52943 postgres: 16/main: student mvcc_freeze [local] idle
  53577 postgres: 16/main: autovacuum worker template0
```
И через некоторое время транзакции окажутся замороженными:
```sql
=> SELECT datname, datfrozenxid, age(datfrozenxid) FROM pg_database;
```

## Page 24: Page 24 {#source-page-024}

```text
   datname   | datfrozenxid | age
-------------+--------------+-----
 postgres    |       100762 |   0
 student     |       100762 |   0
 template1   |       100762 |   0
 template0   |       100762 |   0
 mvcc_freeze |        99829 | 933
(5 rows)
```
