# Вложенные транзакции и savepoints _MVCC и видимость · PostgreSQL Knowledge Base_ **TL;DR:** SAVEPOINT открывает вложенную транзакцию (подтранзакцию) внутри основной. ROLLBACK TO SAVEPOINT откатывает её работу, не трогая остальную транзакцию. Каждая пишущая подтранзакция получает свой xid, а связь «подтранзакция - родитель» хранит pg_subtrans. Блоки EXCEPTION в PL/pgSQL - это тоже подтранзакции. Транзакция в PostgreSQL может откатить не только себя целиком, но и часть своей работы. Инструмент - **точки сохранения** (savepoints), а под капотом у них **подтранзакции**. ## Откат части работы ```sql 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](/courses/postgres/kb/clog-hint-bits.md)). После этого все версии с таким `xmin` становятся невидимыми - правило видимости [xmin-xmax](/courses/postgres/kb/xmin-xmax.md) отсекает их так же, как версии откатившейся обычной транзакции. Физически строки остаются на странице мусором, пока их не уберёт vacuum. Что важно: внешний `pg_current_xact_id()` показывает **верхний** xid транзакции, а не subxid - подтранзакции остаются внутренней механикой. ## EXCEPTION - это тоже подтранзакция Часто подтранзакции появляются неявно. Блок `BEGIN ... EXCEPTION ... END` в PL/pgSQL оборачивается в подтранзакцию: если внутри возникает ошибка, откатывается именно подтранзакция, а обработчик `EXCEPTION` ловит сбой, и основная транзакция продолжается. Без подтранзакции любая ошибка губила бы всю транзакцию целиком. ## Подводный камень: переполнение кэша подтранзакций У каждого бэкенда есть небольшой кэш на 64 активных subxid. Если в одной транзакции открыть больше (тысячи savepoints или вложенных блоков EXCEPTION в цикле), кэш переполняется. Тогда проверка видимости вынуждена ходить в pg_subtrans за каждым subxid, и это заметно тормозит всю систему, а не только виновника. Поэтому массовые savepoints в горячем цикле - это антипаттерн, даже если логически они корректны. ## Команды ```sql SAVEPOINT s1; ``` Открыть точку сохранения (подтранзакцию) внутри транзакции ```sql ROLLBACK TO SAVEPOINT s1; ``` Откатить работу после точки, не завершая транзакцию ```sql RELEASE SAVEPOINT s1; ``` Закрыть точку сохранения, оставив её изменения в транзакции ## См. также - [clog и подсказки фиксации (hint bits)](/courses/postgres/kb/clog-hint-bits.md) - [xmin, xmax и правила видимости](/courses/postgres/kb/xmin-xmax.md) - [Виртуальные и реальные xid](/courses/postgres/kb/virtual-xid.md)