linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть I — Хранение данных

$ глава 6 · 55 минут

Версия строки: заголовок кортежа

Страницу разобрали. Спустимся ещё на уровень - внутрь одной строки. У каждой строки в heap перед данными лежит служебный заголовок, и именно в нём закодировано всё, на чём держится многоверсионность: кто строку создал, кто пометил устаревшей, где её следующая версия.

В этой главе мы декодируем заголовок кортежа через pageinspect и проследим, как UPDATE строит цепочку версий. Это мост к части про MVCC: без понимания заголовка снимки и видимость останутся абстракцией.

6.1 Перед данными - служебный заголовок

Строка в heap хранится как кортеж: служебный заголовок, за ним пользовательские данные. Заголовок занимает 23 байта и выравнивается до 24. В этих байтах - транзакционные штампы, ссылка на следующую версию и флаги. Подробный справочник полей - в tuple-header.

24 байта на каждую строку - это накладной расход многоверсионности. Маленькая строка из одного int (4 байта данных) на диске займёт под 30 байт: заголовок весит больше самих данных. На широких строках эти 24 байта незаметны, на узких - чувствительны.

6.2 Поля заголовка

Прочитать заголовки всех строк на странице можно так:

sql
SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_hoff
FROM heap_page_items(get_raw_page('tup', 0));

Что в каждом поле:

ПолеРазмерСмысл
t_xmin4 байтаxid транзакции, вставившей версию
t_xmax4 байтаxid транзакции, пометившей версию устаревшей (0 - жива)
t_ctid6 байтадрес этой или следующей версии: (страница, указатель)
t_infomask2 байтабитовые флаги состояния и подсказки фиксации
t_hoff1 байтсмещение до пользовательских данных

6.3 xmin и xmax: время жизни версии

t_xmin и t_xmax определяют время жизни версии. xmin - кто её создал, xmax - кто пометил устаревшей. У живой версии xmax = 0.

Эти же поля доступны как системные колонки прямо в выдаче таблицы, без pageinspect:

sql
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 старая версия начинает указывать на новую, и так получается цепочка. Соберём её руками: вставим строку и дважды обновим.

sql
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 точно зафиксировалась?».

sql
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 или вставка дочерней строки с внешним ключом.

sql
-- у строк flights xmax заполнен, но сами строки живы:
-- их «придержала» вставка в tickets по внешнему ключу
SELECT xmin, xmax FROM flights LIMIT 1;

Такой xmax помечен флагом «только блокировка» в infomask, и правило видимости его игнорирует: строка остаётся живой. Поэтому ненулевой xmax сам по себе ещё не значит, что версия мертва - надо смотреть флаги. Мы вернёмся к этому в части про MVCC.

Уроки в sandbox

lab-6.1. Анатомия строки и цепочка версий

Собери цепочку версий руками и декодируй заголовки. Перед запросом heap_page_items предскажи: сколько версий будет на странице, какой ctid у живой, как свяжутся xmax и xmin соседних версий.

  1. Создай таблицу tup(id int, note text) и вставь одну строку (1, 'v1').

  2. Обнови её дважды: сначала на 'v2', потом на 'v3'.

  3. Предскажи, сколько версий лежит на странице 0, и проверь: SELECT count(*) FROM heap_page_items(get_raw_page('tup', 0));.

  4. Посмотри живую версию: SELECT ctid, xmin, xmax FROM tup; - предскажи её ctid до запроса.

  5. Прочитай все версии: SELECT lp, t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('tup',0)) ORDER BY lp; и проверь, что xmax каждой равен xmin следующей.

  6. Проследи 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 может быть блокировкой, а не удалением.

Контрольные вопросы

  1. Какие поля заголовка кортежа отвечают за версионность и что каждое значит?

    Показать ответ

    t_xmin - xid транзакции, вставившей версию; t_xmax - xid транзакции, пометившей её устаревшей (0, если версия жива); t_ctid - адрес версии в формате (страница, номер указателя), указывающий на саму версию или на следующую в цепочке. По паре xmin/xmax вычисляется видимость, а по t_ctid прослеживается цепочка версий после UPDATE.

  2. Что происходит с 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 следующей - это одна транзакция, закрывшая старую версию и открывшая новую.

  3. Почему SELECT показывает одну строку, хотя на странице лежит три версии?

    Показать ответ

    Потому что SELECT показывает только версию, видимую твоей транзакции, а видимой остаётся последняя живая (с xmax = 0). Устаревшие версии физически лежат на странице, но правило видимости их отсекает. Они занимают место как мусор, пока их не уберёт vacuum. Увидеть их можно только заглянув в страницу через pageinspect.

  4. Чему равен t_hoff и как на него влияет NULL в строке?

    Показать ответ

    t_hoff - смещение от начала кортежа до первого байта пользовательских данных. Без NULL заголовок занимает ровно 24 байта, и t_hoff = 24. Если в строке есть NULL, после заголовка добавляется битовая карта NULL (по биту на колонку), и t_hoff вырастает. При этом сам NULL в данных места не занимает - его наличие отмечено только битом в карте.

  5. Почему ненулевой t_xmax не всегда значит, что строка удалена?

    Показать ответ

    Потому что t_xmax ставится не только при удалении или обновлении, но и когда строку блокируют - например, SELECT ... FOR UPDATE или вставка дочерней строки с внешним ключом. Такой xmax помечен в infomask флагом «только блокировка», и правило видимости его игнорирует: строка остаётся живой. Поэтому, увидев ненулевой xmax, нужно смотреть флаги, а не делать вывод «удалена» сразу.

← Предыдущая05-page-anatomyСледующая →07-column-alignment
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки