6.1 Перед данными - служебный заголовок
Строка в heap хранится как кортеж: служебный заголовок, за ним пользовательские данные. Заголовок занимает 23 байта и выравнивается до 24. В этих байтах - транзакционные штампы, ссылка на следующую версию и флаги. Подробный справочник полей - в tuple-header.
24 байта на каждую строку - это накладной расход многоверсионности.
Маленькая строка из одного int (4 байта данных) на диске займёт
под 30 байт: заголовок весит больше самих данных. На широких строках
эти 24 байта незаметны, на узких - чувствительны.
6.2 Поля заголовка
Прочитать заголовки всех строк на странице можно так:
SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_hoff
FROM heap_page_items(get_raw_page('tup', 0));Что в каждом поле:
| Поле | Размер | Смысл |
|---|---|---|
t_xmin | 4 байта | xid транзакции, вставившей версию |
t_xmax | 4 байта | xid транзакции, пометившей версию устаревшей (0 - жива) |
t_ctid | 6 байт | адрес этой или следующей версии: (страница, указатель) |
t_infomask | 2 байта | битовые флаги состояния и подсказки фиксации |
t_hoff | 1 байт | смещение до пользовательских данных |
6.3 xmin и xmax: время жизни версии
t_xmin и t_xmax определяют время жизни версии. xmin - кто её
создал, xmax - кто пометил устаревшей. У живой версии xmax = 0.
Эти же поля доступны как системные колонки прямо в выдаче таблицы, без
pageinspect:
SELECT xmin, xmax, ctid, * FROM tup;
-- xmin | xmax | ctid | id | note
-- ------+------+-------+----+------
-- 797 | 0 | (0,3) | 1 | v3
По паре xmin/xmax транзакция решает, видеть ей версию или нет. Это
и есть фундамент видимости, и ему посвящена отдельная заметка -
xmin-xmax. Здесь важно одно: видимость не хранится флагом, а
вычисляется из этих чисел.
6.4 ctid и цепочка версий
Самое интересное поле - t_ctid. Обычно оно указывает на саму строку.
Но после UPDATE старая версия начинает указывать на новую, и так
получается цепочка. Соберём её руками: вставим строку и дважды обновим.
CREATE TABLE tup (id int, note text);
INSERT INTO tup VALUES (1, 'v1');
UPDATE tup SET note = 'v2' WHERE id = 1;
UPDATE tup SET note = 'v3' WHERE id = 1;
SELECT lp, t_xmin, t_xmax, t_ctid
FROM heap_page_items(get_raw_page('tup', 0)) ORDER BY lp;-- lp | t_xmin | t_xmax | t_ctid
-- ----+--------+--------+--------
-- 1 | 795 | 796 | (0,2) <- v1: устарела (xmax=796), ведёт на (0,2)
-- 2 | 796 | 797 | (0,3) <- v2: устарела (xmax=797), ведёт на (0,3)
-- 3 | 797 | 0 | (0,3) <- v3: жива (xmax=0), ведёт на себя
Смотри, как сходятся числа: xmax каждой версии равен xmin
следующей - это одна и та же транзакция, которая старую версию закрыла,
а новую открыла. А t_ctid ведёт от версии к версии: (0,1) → (0,2) →
(0,3). Конкретные номера транзакций у тебя будут другими, но связь та
же. Сам SELECT * FROM tup показывает только живую версию (0,3) -
остальные ждут, пока их уберёт vacuum.
Указатели строк, на которые ссылается ctid, мы разбирали в
line-pointers: ctid - это пара «страница, номер указателя».
6.5 t_hoff и карта NULL
t_hoff - смещение от начала кортежа до первого пользовательского
байта. Во всех строках выше оно равно 24: заголовок без дополнений.
Но если в строке есть NULL, после заголовка добавляется битовая
карта NULL - по биту на колонку, отмечающему, какие из них пусты.
Тогда t_hoff вырастает. Хитрость: сам NULL в данных места не
занимает, его наличие отмечено битом в карте. Поэтому таблица, где
много пустых колонок, на диске бывает компактнее, чем кажется по числу
колонок.
Где именно лягут колонки за заголовком, определяет их выравнивание - этому посвящена следующая глава, column-alignment.
6.6 infomask и подсказки фиксации
Поле t_infomask - набор битовых флагов. Часть из них описывает саму
строку: есть ли NULL, есть ли поля переменной длины, является ли
xmax блокировкой, а не удалением. Другая часть - подсказки
фиксации (hint bits): закэшированный ответ на вопрос «а транзакция
xmin/xmax точно зафиксировалась?».
SELECT t_infomask::bit(16) FROM heap_page_items(get_raw_page('tup', 0));Подсказки ставит первый, кто прочитал строку после завершения её транзакции, чтобы следующим не лезть за статусом в clog. Это тонкий и неожиданный механизм - почему обычный SELECT может вызвать запись, - и ему посвящена отдельная заметка clog-hint-bits и глава в части про MVCC.
6.7 Подводный камень: ненулевой xmax у живой строки
Увидев xmax, отличный от нуля, легко решить «строка удалена». Не
всегда. xmax ставится не только при удалении или обновлении, но и
когда строку просто блокируют: SELECT ... FOR UPDATE или вставка
дочерней строки с внешним ключом.
-- у строк flights xmax заполнен, но сами строки живы:
-- их «придержала» вставка в tickets по внешнему ключу
SELECT xmin, xmax FROM flights LIMIT 1;
Такой xmax помечен флагом «только блокировка» в infomask, и правило
видимости его игнорирует: строка остаётся живой. Поэтому ненулевой
xmax сам по себе ещё не значит, что версия мертва - надо смотреть
флаги. Мы вернёмся к этому в части про MVCC.
Уроки в sandbox
lab-6.1. Анатомия строки и цепочка версий
Собери цепочку версий руками и декодируй заголовки. Перед запросом
heap_page_items предскажи: сколько версий будет на странице, какой
ctid у живой, как свяжутся xmax и xmin соседних версий.
Создай таблицу
tup(id int, note text)и вставь одну строку(1, 'v1').Обнови её дважды: сначала на
'v2', потом на'v3'.Предскажи, сколько версий лежит на странице 0, и проверь:
SELECT count(*) FROM heap_page_items(get_raw_page('tup', 0));.Посмотри живую версию:
SELECT ctid, xmin, xmax FROM tup;- предскажи еёctidдо запроса.Прочитай все версии:
SELECT lp, t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('tup',0)) ORDER BY lp;и проверь, чтоxmaxкаждой равенxminследующей.Проследи
t_ctidот первой версии к последней - это цепочка (0,1) → (0,2) → (0,3).
sandbox с автопроверкой - открыть в песочнице
Резюме
- Перед данными строки лежит заголовок кортежа: 23 байта, выровненные до 24.
- Ключевые поля: t_xmin (кто вставил), t_xmax (кто пометил устаревшей), t_ctid (адрес этой или следующей версии).
- UPDATE строит цепочку версий: t_ctid ведёт от старой к новой, xmax старой равен xmin новой.
- SELECT показывает только живую версию; устаревшие лежат на странице, пока их не уберёт vacuum.
- t_hoff - смещение до данных (24 без NULL); NULL отмечается битовой картой и места в данных не занимает.
- t_infomask хранит флаги и подсказки фиксации; ненулевой xmax может быть блокировкой, а не удалением.
Контрольные вопросы
Какие поля заголовка кортежа отвечают за версионность и что каждое значит?
Показать ответ
t_xmin- xid транзакции, вставившей версию;t_xmax- xid транзакции, пометившей её устаревшей (0, если версия жива);t_ctid- адрес версии в формате (страница, номер указателя), указывающий на саму версию или на следующую в цепочке. По пареxmin/xmaxвычисляется видимость, а поt_ctidпрослеживается цепочка версий после UPDATE.Что происходит с t_ctid и t_xmax при двух последовательных UPDATE одной строки?
Показать ответ
Образуется цепочка из трёх версий. У первой
t_xmaxравен xid второй транзакции, аt_ctidуказывает на вторую версию. У второйt_xmaxравен xid третьей,t_ctidуказывает на третью. Третья жива:t_xmax= 0,t_ctidуказывает на себя. Получается связь (0,1) → (0,2) → (0,3), причёмxmaxкаждой версии совпадает сxminследующей - это одна транзакция, закрывшая старую версию и открывшая новую.Почему SELECT показывает одну строку, хотя на странице лежит три версии?
Показать ответ
Потому что
SELECTпоказывает только версию, видимую твоей транзакции, а видимой остаётся последняя живая (сxmax = 0). Устаревшие версии физически лежат на странице, но правило видимости их отсекает. Они занимают место как мусор, пока их не уберёт vacuum. Увидеть их можно только заглянув в страницу черезpageinspect.Чему равен t_hoff и как на него влияет NULL в строке?
Показать ответ
t_hoff- смещение от начала кортежа до первого байта пользовательских данных. БезNULLзаголовок занимает ровно 24 байта, иt_hoff = 24. Если в строке естьNULL, после заголовка добавляется битовая карта NULL (по биту на колонку), иt_hoffвырастает. При этом самNULLв данных места не занимает - его наличие отмечено только битом в карте.Почему ненулевой t_xmax не всегда значит, что строка удалена?
Показать ответ
Потому что
t_xmaxставится не только при удалении или обновлении, но и когда строку блокируют - например,SELECT ... FOR UPDATEили вставка дочерней строки с внешним ключом. Такойxmaxпомечен вinfomaskфлагом «только блокировка», и правило видимости его игнорирует: строка остаётся живой. Поэтому, увидев ненулевойxmax, нужно смотреть флаги, а не делать вывод «удалена» сразу.