Learning Platform
Урок 13.03 · 16 мин
Средний
SAVEPOINTPartial rollbackRELEASE SAVEPOINTNested transactions

Зачем нужен SAVEPOINT

В прошлом уроке мы увидели жёсткое правило: любая ошибка переводит транзакцию в aborted state. Все последующие команды отбрасываются, единственный выход — ROLLBACK всей транзакции.

Но иногда это слишком грубо. Представь: ты импортируешь 1000 строк, и одна из них нарушила CHECK. Откатить всю транзакцию — потерять 999 валидных строк. Хочется откатить только сбойную строку и продолжить.

Для таких случаев есть SAVEPOINT — именованная точка внутри транзакции, к которой можно вернуться. Это что-то вроде «закладки в книге»: ты ставишь её, читаешь дальше, и если что — возвращаешься на закладку, не закрывая саму книгу.

ROLLBACK всей транзакции vs ROLLBACK TO SAVEPOINT

Слева: всё откатывается, надо начинать заново. Справа: откатывается только часть после savepoint, остальное в силе.

Полный ROLLBACKтеряем всю работу
BEGIN
UPDATE 1OK
UPDATE 2OK
UPDATE 3ERROR
ROLLBACKоткатили все три UPDATE
SAVEPOINT + ROLLBACK TOтеряем только часть
BEGIN
UPDATE 1OK
UPDATE 2OK
SAVEPOINT sp1закладка
UPDATE 3ERROR
ROLLBACK TO sp1откатили только UPDATE 3
COMMITUPDATE 1 и 2 в силе

Синтаксис

Команд три:

  • SAVEPOINT <name>; — установить закладку с именем <name>.
  • ROLLBACK TO SAVEPOINT <name>; — вернуться к закладке. Все изменения после неё откатываются, но сама транзакция продолжает работать.
  • RELEASE SAVEPOINT <name>; — удалить закладку. Изменения после неё остаются в транзакции, просто эту точку больше нельзя использовать.

RELEASE SAVEPOINT нужен не обязательно — все savepoints автоматически удаляются при COMMIT или ROLLBACK всей транзакции. Но при долгих транзакциях с сотнями savepoints полезно явно их освобождать — каждый savepoint занимает память и слегка замедляет работу.

Базовый паттерн: попробовать опасную операцию, откатиться к savepoint при ошибке

PostgreSQL

Восстановление транзакции после ошибки

Главное практическое применение savepoint — восстановить транзакцию после ошибки, не откатывая всё.

В обычной транзакции после ошибки все следующие команды возвращают «current transaction is aborted, commands ignored until end of transaction block». Но если есть savepoint до ошибки, то ROLLBACK TO SAVEPOINT возвращает транзакцию в нормальное состояние — можно продолжать.

ROLLBACK TO SAVEPOINT снимает aborted state и позволяет продолжить транзакцию

PostgreSQL

Видишь? Первый UPDATE прошёл. INSERT упал. После ROLLBACK TO SAVEPOINT транзакция «ожила», и второй UPDATE тоже прошёл. Если бы мы сделали COMMIT вместо ROLLBACK в конце — оба UPDATE бы зафиксировались.

Pattern: «попробовать N операций, откатить только проблемные»

Классический сценарий — массовый импорт с пропуском плохих строк.

Импорт с savepoint на каждую строку — откатываем только сбойные

PostgreSQL

В реальности эту логику обычно делает не SQL, а драйвер — psycopg, pg, JDBC. Каждая строка превращается в SAVEPOINT row_N; INSERT ...; RELEASE row_N; (или ROLLBACK TO + логирование ошибки). Так работают ORM-уровневые try/except: внутри Python try ORM ставит savepoint, в except — делает ROLLBACK TO. Без этого транзакция в Django/SQLAlchemy после первой же ошибки перестала бы работать.

Savepoints — не вложенные транзакции

Один из частых источников путаницы: savepoints не дают «настоящих» вложенных транзакций. Вот в чём разница.

Если бы вложенные транзакции были «настоящими», внутренняя могла бы закоммититься независимо от внешней — то есть «нижние изменения сохранены, верхние ещё нет». Это поведение в PostgreSQL невозможно — есть только одна реальная транзакция, и она либо вся commit’ится, либо вся откатывается.

Savepoint — это только точка отката. RELEASE SAVEPOINT не «коммитит» подтранзакцию — он просто говорит «эта точка больше не нужна, изменения остаются в общей транзакции». Если потом откатывается основная транзакция — откатываются и все «успешно RELEASE’нутые» изменения.

Это означает, что если вашему приложению нужна истинная вложенная транзакция (commit’нуть часть, оставив остальное живым) — savepoints не подойдут. Нужно архитектурно разделить на две независимые транзакции, например, через очередь сообщений: «первая транзакция commit’ится, посылает событие; вторая обрабатывает событие в своей транзакции».

Вложенные savepoints

Savepoints можно вкладывать друг в друга. Имена должны быть уникальны в рамках одной транзакции — повторное использование имени не запрещено, но «затеняет» предыдущее.

Вложенные savepoints — каждый со своей точкой возврата

PostgreSQL

Это называется «nested transactions» в маркетинге, но строго говоря — это просто иерархия savepoints в рамках одной плоской транзакции. Настоящих вложенных транзакций в стандарте SQL нет.

Стоимость savepoint

Savepoint — не бесплатный. PostgreSQL создаёт

subtransaction
— отдельную запись в pg_subtrans, занимает место в памяти. На паре десятков savepoints это незаметно, на десятках тысяч — может замедлить транзакцию.

Поэтому правило: savepoints — для обработки ошибок и намеренного частичного отката, не для «давайте на всякий случай поставим savepoint после каждой команды». В типичной транзакции их 0–5.

Когда savepoint не помогает

Savepoint работает только внутри одной транзакции. Если транзакция уже сделала COMMIT, откатить её через savepoint нельзя — COMMIT финален.

Также savepoint не защищает от ошибок уровня соединения (потеря TCP, kill -9 клиента). При обрыве соединения PostgreSQL откатывает всю незакомиченную транзакцию целиком, включая все savepoints.

RELEASE SAVEPOINT vs ROLLBACK TO

Две похожие команды, легко перепутать. Разница:

  • RELEASE SAVEPOINT sp — забываем точку, сохраняя все изменения после неё. Закладку убрали, страницы остались.
  • ROLLBACK TO SAVEPOINT sp — возвращаемся к точке, откатывая все изменения после. Закладка остаётся (можно к ней снова откатиться, пока не RELEASE).

Большинство случаев — это RELEASE после успешной операции и ROLLBACK TO при ошибке. Это то, что делают ORM в try/except-блоках.

RELEASE vs ROLLBACK TO: зеркальные операции

RELEASE — успех, закладку убираем, изменения остаются. ROLLBACK TO — неудача, откатываем всё после закладки.

После SAVEPOINT sp
изменения X, Y, Zвсё было успешно
RELEASE SAVEPOINT spзакладка удалена, X, Y, Z остаютсяЭто «всё ок, идём дальше»
при COMMIT — X, Y, Z попадут в базу
После SAVEPOINT sp
изменения X, ERRORчто-то упало
ROLLBACK TO SAVEPOINT spX откачено, savepoint осталсяЭто «что-то пошло не так, давайте откатим только эту часть»
можем продолжать транзакцию — она снова рабочая

Как это работает в популярных ORM

Реальная польза savepoints — даже не SQL-разработчику, а backend-инженеру. Когда ORM делает try/except вокруг группы запросов, под капотом он использует savepoint.

В Django есть transaction.atomic() — этот контекст-менеджер устанавливает savepoint и при exception откатывает к нему. Это означает, что можно вкладывать atomic() друг в друга, и каждый внутренний может откатиться, не разрушая внешний.

with transaction.atomic():
    # внешняя транзакция или внешний savepoint
    user = create_user(...)
    try:
        with transaction.atomic():
            # внутренний savepoint
            send_welcome_email(user)
    except EmailError:
        # внутренний savepoint откачен, но user остался
        log_email_failure(user)
    # commit внешней транзакции — user сохраняется в базу

В SQLAlchemy аналог — session.begin_nested(). Внутренний контекст-менеджер ставит savepoint, исключение делает ROLLBACK TO, успех — RELEASE.

Знание savepoints позволяет понимать, почему такие конструкции работают и где их использовать. В SQL это часто пишется руками, в ORM — это часть API.

Производительность savepoint

Сделаем замер мысленно. Когда ты ставишь savepoint, PostgreSQL:

  1. Назначает subtransaction свой xid (отдельный, но из общей последовательности с обычными транзакциями).
  2. Сохраняет map subtransaction → parent transaction в pg_subtrans.
  3. При чтении строк проверяет visibility с учётом текущей subtransaction.

На паре десятков savepoints это незаметно. На тысячах — особенно если они вложенные — начинают расти затраты на проверку visibility (нужно пройти по цепочке savepoints). Также если транзакция использует больше чем PGPROC_MAX_CACHED_SUBXIDS = 64 subtransactions, переключается на disk-resident map и каждый visibility check становится медленнее.

Правило большого пальца: savepoints — для логически разных стадий или для обработки ошибок, не для «каждой команды». Если делаешь bulk-import — используй COPY и обрабатывай ошибки на уровне приложения, а не savepoint на каждую строку.

Проверка знанийKnowledge check
У нас транзакция: BEGIN, INSERT 1, SAVEPOINT sp, INSERT 2 (с ошибкой CHECK), ROLLBACK TO sp, INSERT 3, COMMIT. Что окажется в базе?
ОтветAnswer
В базе окажутся INSERT 1 и INSERT 3. INSERT 2 откатился вместе с SAVEPOINT sp — это и есть смысл ROLLBACK TO. Важно: без ROLLBACK TO транзакция после ошибочного INSERT 2 была бы в aborted state, и INSERT 3 не выполнился бы. SAVEPOINT + ROLLBACK TO — это механизм «продолжить транзакцию после ошибки». Сам COMMIT в конце фиксирует то, что осталось после всех откатов: то есть строки от INSERT 1 и INSERT 3.
Как savepoints отражаются в снимке MVCC

Чек-лист

  • SAVEPOINT <name> — закладка внутри транзакции; можно поставить много.
  • ROLLBACK TO SAVEPOINT <name> — откатить всё, сделанное после закладки; транзакция остаётся живой, можно продолжать.
  • RELEASE SAVEPOINT <name> — удалить закладку, но сохранить изменения после.
  • Главный кейс — обработка ошибок: после ошибки внутри транзакции ROLLBACK TO снимает aborted state.
  • Savepoints позволяют ORM реализовать try/except на уровне базы.
  • Savepoints стоят память; не злоупотребляй — типично 0–5 на транзакцию.
  • Savepoint работает только внутри одной транзакции — после COMMIT откатить уже нельзя.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём ключевое отличие RELEASE SAVEPOINT от ROLLBACK TO SAVEPOINT?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6