Механизм MVCC
- это механизм управления конкурентным доступом в PostgreSQL, который позволяет многим транзакциям читать и писать данные одновременно — без блокировок и с гарантией изоляции. Каждая транзакция видит согласованную “фотографию” базы на момент своего старта — не мешая другим.
tuple - это фундаментальная единица данных в PostgreSQL, соответствующая одной строке таблицы.
Каждая строка (tuple) в таблице — это не просто ваши данные. В начале каждой строки хранится заголовок (HeapTupleHeader) с системными полями: Размер заголовка — 23–27 байт (в зависимости от флагов).
| Поле | Назначение |
|---|---|
| t_xmin | ID транзакции (XID), которая вставила эту строку |
| t_xmax | ID транзакции, которая удалила или обновила строку (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:
- Смотрит в ProcArray — активна ли транзакция сейчас?
- Если нет — читает CLOG, чтобы узнать: зафиксирована или откачена?
Уровни изоляции и MVCC
PostgreSQL использует MVCC для всех уровней изоляции, кроме SERIALIZABLE (там добавляются дополнительные проверки).
| Уровень | Поведение |
|---|---|
| READ COMMITTED | Получает новый snapshot для каждого запроса → может видеть изменения, сделанные другими транзакциями между запросами. |
| REPEATABLE READ | Использует один snapshot на всю транзакцию → все запросы видят одно и то же состояние. |
| SERIALIZABLE | То же + отслеживает конфликты зависимостей между транзакциями (через pg_serial), чтобы предотвратить аномалии. |