Подготовка

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

=> CREATE DATABASE mvcc_tuples;
CREATE DATABASE
=> \c mvcc_tuples
You are now connected to database "mvcc_tuples" as user "postgres".
=> CREATE TABLE t(id serial, s text);
CREATE TABLE
=> CREATE INDEX t_s on t(s);
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 as xmin,
       t_xmax as xmax,
       CASE WHEN (t_infomask & 256) > 0  THEN 't' END AS xmin_c,
       CASE WHEN (t_infomask & 512) > 0  THEN 't' END AS xmin_a,
       CASE WHEN (t_infomask & 1024) > 0 THEN 't' END AS xmax_c,
       CASE WHEN (t_infomask & 2048) > 0 THEN 't' END AS xmax_a,
       t_ctid
FROM heap_page_items(get_raw_page('t',0))
ORDER BY lp;
CREATE VIEW

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

=> CREATE VIEW t_s_v AS
SELECT itemoffset,
       ctid
FROM bt_page_items('t_s',1);
CREATE VIEW

(Нулевая страница индекса содержит метаинформацию, поэтому смотрим в первую.)


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

=> BEGIN;
BEGIN
=> INSERT INTO t(s) VALUES ('FOO');
INSERT 0 1

Вот номер нашей текущей транзакции:

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


Вот что содержится в странице:

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |    0 |        |        |        | t      | (0,1)
(1 row)

Похожую, но существенно менее детальную информацию можно получить и из самой таблицы, используя псевдостолбцы xmin и xmax:

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
  578 |    0 |  1 | FOO
(1 row)


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

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


Зафиксируем изменение.

=> COMMIT;
COMMIT

Что изменилось?

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |    0 |        |        |        | t      | (0,1)
(1 row)

Ничего, так как единственная операция, которая выполняется при фиксации - запись статуса транзакции в XACT.


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

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |    0 | t      |        |        | t      | (0,1)
(1 row)


Теперь удалим строку.

=> BEGIN;
BEGIN
=> DELETE FROM t;
DELETE 1

Номер транзакции записался в поле xmax:

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |  579 | t      |        |        |        | (0,1)
(1 row)


При откате xmax остается...

=> ROLLBACK;
ROLLBACK
=> SELECT * from t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |  579 | t      |        |        |        | (0,1)
(1 row)


При этом при обращении к странице выставляется соответствующий бит:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |  579 | t      |        |        | t      | (0,1)
(1 row)


Теперь проверим обновление.

=> UPDATE t SET s = 'BAR';
UPDATE 1

Запрос выдает одну строку (новую версию):

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | BAR
(1 row)


Но в странице мы видим обе версии:

=> SELECT * FROM t_v;
 ctid  | state  | xmin | xmax | xmin_c | xmin_a | xmax_c | xmax_a | t_ctid 
-------+--------+------+------+--------+--------+--------+--------+--------
 (0,1) | normal |  578 |  580 | t      |        | t      |        | (0,2)
 (0,2) | normal |  580 |    0 | t      |        |        | t      | (0,2)
(2 rows)

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


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

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

Индексные записи внутри страницы упорядочены. Поэтому первой идет запись с ключом BAR (ссылается на версию (0,2)), а второй - запись с ключом FOO (ссылается на версию (0,1)).


Структура страницы

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

=> SELECT lower, upper, special, pagesize
FROM page_header(get_raw_page('t',0));
 lower | upper | special | pagesize 
-------+-------+---------+----------
    32 |  8128 |    8192 |     8192
(1 row)

Области занимают следующие диапазоны адресов:


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