Механизм MVCC

- это механизм управления конкурентным доступом в PostgreSQL, который позволяет многим транзакциям читать и писать данные одновременно — без блокировок и с гарантией изоляции. Каждая транзакция видит согласованную “фотографию” базы на момент своего старта — не мешая другим.

tuple - это фундаментальная единица данных в PostgreSQL, соответствующая одной строке таблицы.

Каждая строка (tuple) в таблице — это не просто ваши данные. В начале каждой строки хранится заголовок (HeapTupleHeader) с системными полями: Размер заголовка — 23–27 байт (в зависимости от флагов).

ПолеНазначение
t_xminID транзакции (XID), которая вставила эту строку
t_xmaxID транзакции, которая удалила или обновила строку (0 = жива)
t_ctidУказатель (block, offset) на текущую версию строки (для обновлений)
t_infomaskБитовые флаги: зафиксирована ли xmin/xmax, есть ли nulls и т.д.
t_hoffСмещение начала пользовательских данных
struct HeapTupleHeaderData {
    TransactionId t_xmin;   /* 4 байта */
    TransactionId t_xmax;   /* 4 байта */
    CommandId     t_cid;    /* редко используется */
    ItemPointerData t_ctid; /* 6 байт (block + offset) */
    uint16        t_infomask2;
    uint16        t_infomask;
    uint8         t_hoff;
    /* ... данные ... */
};
  • При UPDATE:
    • Старая строка не удаляется, а помечается xmax = current_xid.
    • Создаётся новая строка с xmin = current_xid.
INSERT INTO accounts (id, balance) VALUES (1, 100);
# Создаётся строка с:
t_xmin = 1000 (ID этой/текущей транзакции вставки)
t_xmax = 0 (не удалена)
t_ctid = (42, 5) (указывает сама на себя)
UPDATE accounts SET balance = 150 WHERE id = 1;
# PostgreSQL **не перезаписывает** эту строку.
	# Старая строка:
	t_xmax устанавливается в 1001 (ID текущей транзакции обновления).
	# Она становится «удалённой» для будущих транзакций.
# Новая строка создаётся на той же странице (или другой, если места нет):
t_xmin = 1001
t_xmax = 0
t_ctid = (42, 6) (указывает на себя)
	# Старая строка получает `t_ctid = (42, 6)` → указывает на новую версию.
 
Это называется **HOT (Heap-Only Tuple) update**, если не затронуты индексы.

Видимость строки

Когда транзакция читает данные, она не просто берёт все строки. Она использует снимок (snapshot) и глобальные структуры, чтобы решить: видна ли строка?

Snapshot (снимок)

При старте транзакции (или при первом запросе в READ COMMITTED) PostgreSQL создаёт снимок видимости:

struct SnapshotData {
    TransactionId xmin;     // самые старые "живые" XID
    TransactionId xmax;     // самый новый XID + 1
    TransactionId *xip;     // массив активных XID на момент снимка
    uint32        xcnt;     // их количество
}

Например: xmin=990, xmax=1005, xip = [1000, 1002, 1004]

  • все транзакции с XID < 990зафиксированы.
  • все с XID >= 1005ещё не начались.
  • а 1000, 1002, 1004активны, их результаты невидимы.
# Для строки с `t_xmin = A`, `t_xmax = B`:
	# Строка вставлена?
		Если `A >= xmax` строка ещё не существует* невидима.
	    Если `A` в `xip` транзакция вставки активна невидима.
	    Если `A < xmin` транзакция зафиксирована и стара видима (если не удалена).
	# Строка удалена/обновлена?
	    Если `B == 0` не удалена видима (если прошла п.1).
	    Если `B >= xmax` удаление ещё не произошло видима.
	    Если `B` в `xip` транзакция удаления активна видима.
	    Если `B < xmin` удаление зафиксировано невидима.
 
> Это реализовано в функции `HeapTupleSatisfiesMVCC()` в исходном коде PostgreSQL.

Проблема: раздувание таблицы

  • Мёртвые строки (устаревшие версии) остаются на диске.
  • Их убирает VACUUM (обычно — auto vacuum).

Информация о статусе транзакций?

xmin/xmax — это просто числа. Но откуда PostgreSQL узнаёт: **зафиксирована ли транзакция с XID=1000?

ProcArray ^psql-ProcArray (быстро, в памяти), CLOG (Commit Log) ^psql-clog (на диске, но с кэшем).

При проверке видимости PostgreSQL:

  1. Смотрит в ProcArray — активна ли транзакция сейчас?
  2. Если нет — читает CLOG, чтобы узнать: зафиксирована или откачена?

Уровни изоляции и MVCC

PostgreSQL использует MVCC для всех уровней изоляции, кроме SERIALIZABLE (там добавляются дополнительные проверки).

УровеньПоведение
READ COMMITTEDПолучает новый snapshot для каждого запроса → может видеть изменения, сделанные другими транзакциями между запросами.
REPEATABLE READИспользует один snapshot на всю транзакцию → все запросы видят одно и то же состояние.
SERIALIZABLEТо же + отслеживает конфликты зависимостей между транзакциями (через pg_serial), чтобы предотвратить аномалии.