Транзакция в PostgreSQL может откатить не только себя целиком, но и часть своей работы. Инструмент - точки сохранения (savepoints), а под капотом у них подтранзакции.
Откат части работы
BEGIN;
INSERT INTO mv VALUES (2, 'sub');
SAVEPOINT s1;
INSERT INTO mv VALUES (3, 'oops');
ROLLBACK TO SAVEPOINT s1; -- отменили только вставку (3,'oops')
COMMIT; -- (2,'sub') осталась, (3,'oops') - нет
ROLLBACK TO SAVEPOINT отматывает всё, что случилось после точки, но сама
транзакция продолжает жить. Это не то же самое, что полный ROLLBACK:
работа до точки сохранения цела.
Что происходит внутри
Каждая точка сохранения открывает подтранзакцию. Как только подтранзакция
что-то пишет, ей выдаётся собственный xid (subxid). Версии строк,
созданные внутри неё, помечаются этим subxid в xmin. Связь «этот subxid
принадлежит такой-то родительской транзакции» хранит каталог
pg_subtrans.
Когда подтранзакция откатывается, её subxid помечается как aborted в clog
(см. clog-hint-bits). После этого все версии с таким xmin становятся
невидимыми - правило видимости xmin-xmax отсекает их так же, как
версии откатившейся обычной транзакции. Физически строки остаются на
странице мусором, пока их не уберёт vacuum.
Что важно: внешний pg_current_xact_id() показывает верхний xid
транзакции, а не subxid - подтранзакции остаются внутренней механикой.
EXCEPTION - это тоже подтранзакция
Часто подтранзакции появляются неявно. Блок BEGIN ... EXCEPTION ... END
в PL/pgSQL оборачивается в подтранзакцию: если внутри возникает ошибка,
откатывается именно подтранзакция, а обработчик EXCEPTION ловит сбой, и
основная транзакция продолжается. Без подтранзакции любая ошибка губила бы
всю транзакцию целиком.
Подводный камень: переполнение кэша подтранзакций
У каждого бэкенда есть небольшой кэш на 64 активных subxid. Если в одной транзакции открыть больше (тысячи savepoints или вложенных блоков EXCEPTION в цикле), кэш переполняется. Тогда проверка видимости вынуждена ходить в pg_subtrans за каждым subxid, и это заметно тормозит всю систему, а не только виновника. Поэтому массовые savepoints в горячем цикле - это антипаттерн, даже если логически они корректны.