Заморозка

Установим для демонстрации параметры заморозки.

Небольшой возраст транзакции:

=> ALTER SYSTEM SET vacuum_freeze_min_age = 1;
ALTER SYSTEM

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

=> ALTER SYSTEM SET vacuum_freeze_table_age = 3;
ALTER SYSTEM

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

=> ALTER SYSTEM SET autovacuum = off;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)


Создадим таблицу с данными. Установим минимальный fillfactor: на каждой странице будет всего две строки.

=> CREATE DATABASE mvcc_freeze;
CREATE DATABASE
=> \c mvcc_freeze
You are now connected to database "mvcc_freeze" as user "postgres".
=> CREATE TABLE t(id integer, s char(300)) WITH (fillfactor = 10);
CREATE TABLE

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

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

=> CREATE EXTENSION pageinspect;
CREATE EXTENSION
=> 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,
       t_ctid
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;
CREATE VIEW

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

=> CREATE EXTENSION pg_visibility;
CREATE EXTENSION

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

=> INSERT INTO t(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id);
INSERT 0 100
=> VACUUM t;
VACUUM

Сразу после очистки обе страницы отмечены в карте видимости (all_visible):

=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen 
-------+-------------+------------
     0 | t           | f
     1 | t           | f
(2 rows)


Каков возраст транзакции, создавшей строки?

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmin_age | xmin_c | xmin_a | xmax | t_ctid 
-------+--------+------+----------+--------+--------+------+--------
 (0,1) | normal |  655 |        1 | t      |        |    0 | (0,1)
 (0,2) | normal |  655 |        1 | t      |        |    0 | (0,2)
 (1,1) | normal |  655 |        1 | t      |        |    0 | (1,1)
 (1,2) | normal |  655 |        1 | t      |        |    0 | (1,2)
(4 rows)

Возраст равен 1; версии строк с такой транзакцией еще не будут заморожены.


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

=> UPDATE t SET s = 'BAR' WHERE id = 1;
UPDATE 1
=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmin_age | xmin_c | xmin_a | xmax | t_ctid 
-------+--------+------+----------+--------+--------+------+--------
 (0,1) | normal |  655 |        2 | t      |        |  656 | (0,3)
 (0,2) | normal |  655 |        2 | t      |        |    0 | (0,2)
 (0,3) | normal |  656 |        1 |        |        |    0 | (0,3)
 (1,1) | normal |  655 |        2 | t      |        |    0 | (1,1)
 (1,2) | normal |  655 |        2 | t      |        |    0 | (1,2)
(5 rows)


Сейчас нулевая страница уже будет обработана заморозкой:

=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen 
-------+-------------+------------
     0 | f           | f
     1 | t           | f
(2 rows)


Выполняем очистку.

=> VACUUM t;
VACUUM
=> SELECT * FROM t_v;
 ctid  |     state     | xmin | xmin_age | xmin_c | xmin_a | xmax | t_ctid 
-------+---------------+------+----------+--------+--------+------+--------
 (0,1) | redirect to 3 |      |          |        |        |      | 
 (0,2) | normal        |  655 |        2 | t      | t      |    0 | (0,2)
 (0,3) | normal        |  656 |        1 | t      |        |    0 | (0,3)
 (1,1) | normal        |  655 |        2 | t      |        |    0 | (1,1)
 (1,2) | normal        |  655 |        2 | t      |        |    0 | (1,2)
(5 rows)

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


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

=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen 
-------+-------------+------------
     0 | t           | f
     1 | t           | f
(2 rows)


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

Для каждой таблицы сохраняется номер самой последней (наименее старой) замороженной транзакции. Ее возраст и сравнивается со значением параметра.

=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
 relfrozenxid | age 
--------------+-----
          655 |   2
(1 row)


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

=> SELECT txid_current();
 txid_current 
--------------
          657
(1 row)


=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
 relfrozenxid | age 
--------------+-----
          655 |   3
(1 row)

=> VACUUM t;
VACUUM

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

=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 't';
 relfrozenxid | age 
--------------+-----
          657 |   1
(1 row)


Вот что получилось в страницах:

=> SELECT * FROM t_v;
 ctid  |     state     | xmin | xmin_age | xmin_c | xmin_a | xmax | t_ctid 
-------+---------------+------+----------+--------+--------+------+--------
 (0,1) | redirect to 3 |      |          |        |        |      | 
 (0,2) | normal        |  655 |        3 | t      | t      |    0 | (0,2)
 (0,3) | normal        |  656 |        2 | t      | t      |    0 | (0,3)
 (1,1) | normal        |  655 |        3 | t      | t      |    0 | (1,1)
 (1,2) | normal        |  655 |        3 | t      | t      |    0 | (1,2)
(5 rows)


Обе страницы теперь отмечены в карте заморозки.

=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('t',g.blkno)
ORDER BY g.blkno;
 blkno | all_visible | all_frozen 
-------+-------------+------------
     0 | t           | t
     1 | t           | t
(2 rows)


Номер последней замороженной транзакции есть и на уровне всей БД:

=> SELECT datfrozenxid, age(datfrozenxid)
FROM pg_database WHERE datname = 'postgres';
 datfrozenxid | age 
--------------+-----
          548 | 110
(1 row)

Он устанавливается в минимальное значение из relfrozenxid всех таблиц этой БД. Если возраст datfrozenxid превысит значение параметра autovacuum_freeze_max_age, автоочистка будет запущена принудительно.


Конец демонстрации.