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скоро
  • Уроки
  • База знаний
  • Собеседование
Часть II — Многоверсионность (MVCC)

$ глава 12 · 50 минут

clog, подсказки фиксации и вложенные транзакции

Мы много раз говорили: версия видна, если её транзакция зафиксирована. Но где хранится сам факт «зафиксирована или откатилась»? В полях xmin/xmax его нет - там только номера. Эта глава отвечает на вопрос, как сервер узнаёт статус транзакции, причём дёшево, и почему из-за этого обычный SELECT иногда вызывает запись.

Заодно закроем тему вложенных транзакций - savepoints. А в лабе поймаем тот самый момент, когда чтение меняет страницу: увидим, как первый SELECT расставляет подсказки фиксации.

12.1 Зафиксирована или нет: вопрос на каждую строку

Поля xmin и xmax - это просто номера транзакций (см. xmin-xmax). По ним нельзя сказать, чем транзакция кончилась: коммитом или откатом. А правило видимости спрашивает это постоянно, на каждую проверяемую версию: «транзакция, создавшая её, точно закоммитилась?»

Хранить ответ прямо в строке нельзя: в момент создания версии её транзакция ещё не знает своей судьбы. Значит, статус живёт отдельно, и его надо где-то быстро находить. Это «где-то» называется clog.

12.2 clog: два бита на транзакцию

Статус каждой транзакции хранится в clog - каталоге pg_xact/ внутри PGDATA. На транзакцию приходится всего два бита:

БитыСтатус
00в процессе
01зафиксирована
10откатилась
11подтранзакция (промежуточно)

Два бита на транзакцию - это очень компактно. Статусы миллионов транзакций умещаются в мегабайты, а горячая часть кэшируется в памяти. Когда правило видимости спрашивает «xmin закоммитился?», ответ ищется в clog по номеру транзакции.

12.3 Подсказки фиксации

Лезть в clog на каждую строку при каждом чтении дорого. Поэтому ответ кэшируется прямо в строке - подсказками фиксации (hint bits) в поле t_infomask (см. tuple-header).

Флаг «xmin точно зафиксирован» ставит первый, кто прочитал строку после завершения её транзакции. Дальше clog для этой строки не тревожат: ответ уже в ней. Подробнее - в clog-hint-bits.

12.4 SELECT, который пишет

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

sql
CREATE TABLE hb (id int);
INSERT INTO hb VALUES (1);
-- сразу после вставки: бит HEAP_XMIN_COMMITTED (256) ещё не стоит
SELECT (t_infomask & 256) FROM heap_page_items(get_raw_page('hb', 0));
-- 0
SELECT * FROM hb;     -- обычное чтение проверяет видимость и ставит подсказку
-- теперь бит стоит
SELECT (t_infomask & 256) FROM heap_page_items(get_raw_page('hb', 0));
-- 256

Обычный SELECT * FROM hb поменял страницу: бит HEAP_XMIN_COMMITTED перешёл из 0 в 256. А раз страница изменилась - она помечена грязной и будет записана на диск. Получается, безобидное чтение вызвало запись.

12.5 Почему это важно на практике

Эффект «SELECT, который пишет» объясняет загадочную просадку производительности. Сразу после массовой загрузки данных первый запрос, который их читает, расставляет подсказки фиксации по всем свежим строкам - и вызывает шквал записи. Поэтому «холодное» первое чтение только что загруженной таблицы бывает заметно дороже последующих.

Это не утечка и не баг, а отложенная работа: фиксацию статуса размазывают по первым чтениям, вместо того чтобы делать её всю при коммите. Зная это, не пугаешься, увидев запись от чисто читающего запроса в мониторинге сразу после загрузки.

12.6 Вложенные транзакции: savepoints

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

sql
BEGIN;
INSERT INTO hb VALUES (2);
SAVEPOINT s1;
INSERT INTO hb VALUES (3);
ROLLBACK TO SAVEPOINT s1;   -- отменили только вставку 3
COMMIT;                     -- 2 осталась, 3 - нет

Каждая пишущая подтранзакция получает свой xid, а связь «подтранзакция - родитель» хранит каталог pg_subtrans. Версии, созданные в откаченной подтранзакции, становятся невидимы - правило видимости отсекает их так же, как версии откатившейся обычной транзакции. Подробно - в subtransactions.

12.7 Подводный камень: блоки EXCEPTION в цикле

Подтранзакции возникают и неявно. Блок BEGIN ... EXCEPTION ... END в PL/pgSQL оборачивается в подтранзакцию: если внутри ошибка, откатывается она, а не вся транзакция. Удобно - но у этого есть цена.

У каждого backend'а небольшой кэш на 64 активные подтранзакции. Если открыть больше (тысячи savepoints или блоков EXCEPTION в горячем цикле), кэш переполняется, и проверка видимости начинает ходить в pg_subtrans за каждым номером. Это тормозит всю систему, а не только виновника. Поэтому массовые подтранзакции в цикле - антипаттерн, даже когда логически они корректны. На этом часть про MVCC завершается: дальше мы разберём, кто и как убирает мусор, который вся эта многоверсионность оставляет.

Уроки в sandbox

lab-12.1. Подсказки фиксации своими глазами

Поймай момент, когда чтение меняет страницу. Вставь строку, посмотри на её infomask до и после обычного SELECT. Перед каждым замером предскажи: стоит ли бит HEAP_XMIN_COMMITTED.

  1. Создай таблицу hb(id int) и вставь одну строку (1).

  2. Предскажи: стоит ли уже бит фиксации? Проверь сырую страницу: SELECT (t_infomask & 256) FROM heap_page_items(get_raw_page('hb', 0)); - ожидай 0.

  3. Выполни обычное чтение: SELECT * FROM hb; - оно проверяет видимость и расставляет подсказки.

  4. Снова посмотри бит: SELECT (t_infomask & 256) FROM heap_page_items(get_raw_page('hb', 0)); - предскажи, изменился ли он.

  5. Объясни, почему обычный SELECT привёл к изменению страницы (а значит, к будущей записи на диск).

  6. Посмотри весь infomask в битах: SELECT t_infomask::bit(16) FROM heap_page_items(get_raw_page('hb', 0)); и найди выставленные флаги.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Статус транзакции (зафиксирована/откатилась) хранится не в строке, а в clog (каталог pg_xact) - два бита на транзакцию.
  • Правило видимости спрашивает clog по номеру транзакции, чтобы узнать, можно ли доверять xmin/xmax.
  • Подсказки фиксации в t_infomask кэшируют ответ в строке, чтобы не ходить в clog при каждом чтении.
  • Подсказку ставит первое чтение строки после завершения её транзакции, и это меняет страницу: чтение вызывает запись.
  • Первый SELECT после массовой загрузки дороже последующих, потому что расставляет подсказки по всем строкам.
  • Savepoints открывают подтранзакции (pg_subtrans) и позволяют откатить часть работы; блоки EXCEPTION - тоже подтранзакции.

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

  1. Где хранится статус транзакции и почему не прямо в строке?

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

    В clog - каталоге pg_xact/, по два бита на транзакцию (в процессе, зафиксирована, откатилась, подтранзакция). Прямо в строке его держать нельзя: в момент создания версии её транзакция ещё не знает своей судьбы (закоммитится или откатится). Поэтому статус хранится отдельно, а правило видимости находит его в clog по номеру транзакции из xmin/xmax.

  2. Что такое подсказки фиксации и зачем они нужны?

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

    Подсказки фиксации (hint bits) - это флаги в t_infomask строки, которые кэшируют ответ clog: «транзакция xmin/xmax точно зафиксирована (или откатилась)». Они нужны, чтобы не обращаться в clog при каждом чтении строки: первый прочитавший ставит подсказку, а все последующие читают статус прямо из строки. Это экономит массу обращений к clog на горячих таблицах.

  3. Почему обычный SELECT может вызвать запись на диск?

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

    Потому что при первом чтении строки после завершения её транзакции SELECT проверяет статус в clog и ставит подсказку фиксации в t_infomask. Установка подсказки меняет байты страницы, а значит, помечает её грязной, и страница будет записана на диск. Это видно на эксперименте: бит HEAP_XMIN_COMMITTED переходит из 0 в 256 после обычного SELECT. Чтение вызвало запись.

  4. Почему первое чтение только что загруженной таблицы заметно дороже?

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

    Потому что оно расставляет подсказки фиксации сразу по всем свежим строкам, и каждая установка делает страницу грязной. Получается шквал записи от чисто читающего запроса. Это отложенная работа: фиксацию статуса не делают всю разом при коммите, а размазывают по первым чтениям. Поэтому холодный первый SELECT после массовой загрузки дороже последующих, когда подсказки уже расставлены.

  5. Как savepoint позволяет откатить часть транзакции?

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

    SAVEPOINT открывает подтранзакцию внутри основной. ROLLBACK TO SAVEPOINT отматывает всё, что сделано после точки, но сама транзакция продолжает жить - работа до точки цела. Каждая пишущая подтранзакция получает свой xid, а связь с родителем хранит pg_subtrans. Версии, созданные в откаченной подтранзакции, становятся невидимыми по тому же правилу видимости, что и версии откатившейся обычной транзакции.

← Предыдущая11-isolation-levelsСледующая →13-pruning-hot
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки