Настоящих идентификаторов транзакций (xid) ограниченное число: счётчик
32-битный, около четырёх миллиардов значений, и он по кругу
переиспользуется. Если бы каждая транзакция, включая голые SELECT,
сжигала по xid, счётчик летел бы вперёд и заморозка с борьбой за
wraparound случались бы куда чаще.
PostgreSQL экономит. Транзакция получает реальный xid не сразу, а только по необходимости.
Два идентификатора
| Идентификатор | Когда выдаётся | Чего стоит |
|---|---|---|
| виртуальный (vxid) | при старте любой транзакции | ничего, переиспользуется |
| реальный (xid) | при первой записи | расходует счётчик транзакций |
Виртуальный xid - это пара «номер бэкенда / локальный счётчик»,
например 3/12. Он выдаётся мгновенно и бесплатно, живёт только пока
открыта транзакция, и после неё номер свободно переиспользуется. Глобальный
счётчик транзакций он не двигает.
Реальный xid нужен, только когда транзакция собирается оставить след в
данных: вставить, обновить или удалить строку. Тогда ей нужно проштамповать
версии своим xmin/xmax (см. xmin-xmax), а для этого требуется
настоящий номер.
Увидеть разницу
BEGIN;
SELECT pg_current_xact_id_if_assigned(); -- пусто: только читали, xid не выдан
INSERT INTO mv VALUES (9, 'write');
SELECT pg_current_xact_id_if_assigned(); -- теперь число: запись потребовала xid
COMMIT;
Пока транзакция ничего не записала, pg_current_xact_id_if_assigned()
возвращает пусто - реального xid у неё нет. Первая же запись его назначает.
Функция pg_current_xact_id() назначила бы xid принудительно, поэтому в
диагностике берут вариант ..._if_assigned, чтобы случайно не сжечь номер.
Зачем это знать
Из этого следуют две практичные вещи. Первая: длинная читающая
транзакция не двигает счётчик и сама по себе не приближает wraparound
(хотя горизонт всё равно держит - но это про vacuum). Вторая: в pg_locks
у транзакции всегда есть строка virtualxid, а строка transactionid
появляется только после первой записи. Поэтому по наличию реального xid
можно понять, писала транзакция или только читала.