HOT-обновление

Создадим таблицу с двумя полями и индекс.

Параметр fillfactor установим в 75 %.

=> CREATE DATABASE mvcc_hot;
CREATE DATABASE
=> \c mvcc_hot
You are now connected to database "mvcc_hot" as user "postgres".
=> CREATE TABLE t(id integer, s char(2000))
WITH (fillfactor = 75);
CREATE TABLE
=> CREATE INDEX t_id ON t(id);
CREATE INDEX

Как обычно, используем pageinspect.

=> CREATE EXTENSION pageinspect;
CREATE EXTENSION
=> 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 || CASE
         WHEN (t_infomask & 256) > 0 THEN ' (c)'
         WHEN (t_infomask & 512) > 0 THEN ' (a)'
         ELSE ''
       END AS xmin,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' (c)'
         WHEN (t_infomask & 2048) > 0 THEN ' (a)'
         ELSE ''
       END AS xmax,
       CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu,
       CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot,
       t_ctid
FROM heap_page_items(get_raw_page('t',0))
ORDER BY lp;
CREATE VIEW
=> CREATE VIEW t_id_v AS
SELECT itemoffset,
       ctid
FROM bt_page_items('t_id',1);
CREATE VIEW

Вставляем строку и обновляем ее.

=> INSERT INTO t(s) VALUES ('A');
INSERT 0 1
=> UPDATE t SET s = 'B';
UPDATE 1
=> UPDATE t SET s = 'C';
UPDATE 1
=> UPDATE t SET s = 'D';
UPDATE 1

В индексной странице только одна ссылка на таблицу:

=> SELECT * FROM t_id_v;
 itemoffset | ctid  
------------+-------
          1 | (0,1)
(1 row)

В табличной странице - версии строки, объединенные в список:

=> SELECT * FROM t_v;
 ctid  | state  |   xmin    |   xmax    | hhu | hot | t_ctid 
-------+--------+-----------+-----------+-----+-----+--------
 (0,1) | normal | 55487 (c) | 55488 (c) | t   |     | (0,2)
 (0,2) | normal | 55488 (c) | 55489 (c) | t   | t   | (0,3)
 (0,3) | normal | 55489 (c) | 55490     | t   | t   | (0,4)
 (0,4) | normal | 55490     | 0 (a)     |     | t   | (0,4)
(4 rows)

HOT-очистка

Как и в демонстрации, еще одно обновление строки приводит к внутристраничной очистке:

=> UPDATE t SET s = 'E';
UPDATE 1
=> SELECT * FROM t_v;
 ctid  |     state     |   xmin    | xmax  | hhu | hot | t_ctid 
-------+---------------+-----------+-------+-----+-----+--------
 (0,1) | redirect to 4 |           |       |     |     | 
 (0,2) | normal        | 55491     | 0 (a) |     | t   | (0,2)
 (0,3) | unused        |           |       |     |     | 
 (0,4) | normal        | 55490 (c) | 55491 | t   | t   | (0,2)
(4 rows)

Обратите внимание, что:

Выполним обновление еще несколько раз:

=> UPDATE t SET s = 'F';
UPDATE 1
=> UPDATE t SET s = 'G';
UPDATE 1
=> SELECT * FROM t_v;
 ctid  |     state     |   xmin    |   xmax    | hhu | hot | t_ctid 
-------+---------------+-----------+-----------+-----+-----+--------
 (0,1) | redirect to 4 |           |           |     |     | 
 (0,2) | normal        | 55491 (c) | 55492 (c) | t   | t   | (0,3)
 (0,3) | normal        | 55492 (c) | 55493     | t   | t   | (0,5)
 (0,4) | normal        | 55490 (c) | 55491 (c) | t   | t   | (0,2)
 (0,5) | normal        | 55493     | 0 (a)     |     | t   | (0,5)
(5 rows)

Следующее обновление снова вызывает очистку:

=> UPDATE t SET s = 'H';
UPDATE 1
=> SELECT * FROM t_v;
 ctid  |     state     |   xmin    | xmax  | hhu | hot | t_ctid 
-------+---------------+-----------+-------+-----+-----+--------
 (0,1) | redirect to 5 |           |       |     |     | 
 (0,2) | normal        | 55494     | 0 (a) |     | t   | (0,2)
 (0,3) | unused        |           |       |     |     | 
 (0,4) | unused        |           |       |     |     | 
 (0,5) | normal        | 55493 (c) | 55494 | t   | t   | (0,2)
(5 rows)

Разрыв HOT-цепочки

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

=> \c mvcc_hot
You are now connected to database "mvcc_hot" as user "postgres".
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT count(*) FROM t;
 count 
-------
     1
(1 row)

Теперь выполняем обновление в первом сеансе:

=> UPDATE t SET s = 'I';
UPDATE 1
=> UPDATE t SET s = 'J';
UPDATE 1
=> UPDATE t SET s = 'K';
UPDATE 1
=> SELECT * FROM t_v;
 ctid  |     state     |   xmin    |   xmax    | hhu | hot | t_ctid 
-------+---------------+-----------+-----------+-----+-----+--------
 (0,1) | redirect to 2 |           |           |     |     | 
 (0,2) | normal        | 55494 (c) | 55495 (c) | t   | t   | (0,3)
 (0,3) | normal        | 55495 (c) | 55496 (c) | t   | t   | (0,4)
 (0,4) | normal        | 55496 (c) | 55497     | t   | t   | (0,5)
 (0,5) | normal        | 55497     | 0 (a)     |     | t   | (0,5)
(5 rows)

Следующее обновление уже не сможет освободить место на странице:

=> UPDATE t SET s = 'L';
UPDATE 1
=> SELECT * FROM t_v;
 ctid  |     state     |   xmin    |   xmax    | hhu | hot | t_ctid 
-------+---------------+-----------+-----------+-----+-----+--------
 (0,1) | redirect to 2 |           |           |     |     | 
 (0,2) | normal        | 55494 (c) | 55495 (c) | t   | t   | (0,3)
 (0,3) | normal        | 55495 (c) | 55496 (c) | t   | t   | (0,4)
 (0,4) | normal        | 55496 (c) | 55497 (c) | t   | t   | (0,5)
 (0,5) | normal        | 55497 (c) | 55498     |     | t   | (1,1)
(5 rows)

Видим ссылку (1,1), ведущую на страницу 1.

Теперь в индексе - две строки, каждая указывает на начало своей HOT-цепочки:

=> SELECT * FROM t_id_v;
 itemoffset | ctid  
------------+-------
          1 | (1,1)
          2 | (0,1)
(2 rows)

=> COMMIT;
COMMIT