Learning Platform
Урок 13.02 · 16 мин
Средний
BEGINCOMMITROLLBACKAutocommitImplicit transaction

В PostgreSQL транзакции всегда

Это удивляет новичков: в PostgreSQL каждая команда — это уже транзакция. Когда ты пишешь SELECT * FROM customers; — за кулисами происходит:

  1. BEGIN (неявный).
  2. Сам SELECT.
  3. COMMIT (неявный).

Это режим называется

autocommit
. Альтернатива — явная транзакция, которая открывается командой BEGIN (или эквивалентным START TRANSACTION) и закрывается одной из двух команд: COMMIT фиксирует изменения, ROLLBACK откатывает.

Внутри явной транзакции тоже всё транзакционно: каждый отдельный UPDATE либо применяется ко всем выбранным строкам, либо ни к одной. Но все эти отдельные UPDATE остаются «в воздухе» до общего COMMIT — для других сессий они невидимы.

Autocommit vs явная транзакция

Слева: каждая команда — своя транзакция. Справа: одна транзакция вокруг трёх команд, commit один.

Autocommit3 транзакции = 3 fsync
UPDATE accounts ... (commit)
UPDATE accounts ... (commit)
UPDATE accounts ... (commit)
между ними другие сессии видят промежуточные состояния
Явная транзакция1 транзакция = 1 fsync
BEGIN
UPDATE accounts ...
UPDATE accounts ...
UPDATE accounts ...
COMMIT (или ROLLBACK)

BEGIN, COMMIT, ROLLBACK

Синтаксис простой:

  • BEGIN; — открыть транзакцию. В стандарте SQL официально START TRANSACTION, но PostgreSQL принимает оба варианта.
  • COMMIT; — зафиксировать изменения. После COMMIT они видны всем остальным сессиям и переживут падение базы.
  • ROLLBACK; — отменить транзакцию. Все изменения внутри неё откатываются, как будто их не было.

Если в открытой транзакции ты разорвал соединение (например, упал клиент) — PostgreSQL автоматически делает ROLLBACK. Незакоммиченное никогда не сохраняется.

Простая транзакция с COMMIT — изменения остаются:

PostgreSQL

Та же транзакция, но с ROLLBACK — изменения откатываются:

PostgreSQL

Заметь интересную асимметрию: внутри транзакции ты видишь свои собственные изменения, как будто они уже применены. Снаружи (другие сессии) их не видят до COMMIT. Это поведение называется

«read your own writes»
— твоя транзакция работает в своём приватном мирке, в котором её изменения уже есть.

Что видно внутри транзакции — а что нет

Это ключевой момент, который часто путают.

Свои изменения видны сразу. Если ты в одной сессии сделал UPDATE, следующий SELECT в этой же сессии (даже без commit) увидит обновлённые данные.

Чужие изменения — зависит от уровня изоляции. На дефолтном READ COMMITTED ты видишь чужие commit’нутые изменения сразу. На REPEATABLE READ — только те, что были commit’нуты до начала твоей транзакции. Об этом — урок 5.

Незакоммиченные чужие изменения — никогда. PostgreSQL вообще не имеет уровня READ UNCOMMITTED, на котором были бы видны чужие непакоммиченные данные. То есть «dirty read» (грязное чтение) в PostgreSQL невозможно по умолчанию, на любом уровне.

Внутри транзакции ты видишь свои собственные изменения, как будто они уже применились:

PostgreSQL

Когда нужна явная транзакция

Главное правило: группа связанных изменений = одна транзакция. Если есть бизнес-инвариант, который должен сохраняться между несколькими UPDATE/INSERT/DELETE — оберни всё в BEGIN...COMMIT.

Типичные случаи:

  1. Перевод между сущностями. Списать с одного аккаунта, зачислить на другой. Либо обе операции, либо ни одной.
  2. Создание заказа. INSERT INTO orders, потом INSERT INTO order_items несколько раз, потом UPDATE products SET in_stock = in_stock - qty. Все вместе.
  3. Миграции схемы. ALTER TABLE, UPDATE для backfill, ALTER TABLE для NOT NULL. Если что-то упало — нужно вернуться к старой схеме целиком.
  4. Импорт данных. Если импортируешь 100 000 строк и на 99 999 случилась ошибка — должно ли остаться 99 998 строк? Обычно нет.

Реалистичная транзакция: создаём заказ и резервируем товар одной транзакцией:

PostgreSQL

Что было бы без транзакции: между INSERT INTO orders и UPDATE products мог случиться сбой, и мы получили бы заказ на товар, остаток которого не уменьшился — то есть один товар мы продали бы дважды.

Транзакция и пул соединений

Современные приложения используют пул соединений (PgBouncer, HikariCP, asyncpg pool). Это важно знать, чтобы не наступить на грабли.

В режиме session pooling (как в PgBouncer session mode) каждое приложение-соединение мапится на одно серверное соединение на всё время. Транзакции работают как обычно.

В режиме transaction pooling (PgBouncer transaction mode — default) серверное соединение возвращается в пул после каждого COMMIT/ROLLBACK. Это значит, что нельзя использовать:

  • Сессионные переменные (SET ... без LOCAL).
  • LISTEN/NOTIFY.
  • Курсоры между транзакциями.
  • Подготовленные выражения (PREPARE) между транзакциями.

Если вы видите странное поведение «работает локально, не работает в production» — проверьте, не использует ли production transaction pooling.

В режиме statement pooling не работают даже транзакции — каждое выражение в своём соединении. Это режим только для конкретных случаев (например, простой read-only proxy).

Знание этого важно: ваше приложение и PostgreSQL общаются через пул, и от настроек пула зависят гарантии транзакций.

ROLLBACK как «undo»

Есть один полезный shortcut, которым я пользуюсь регулярно: BEGIN; ... SELECT to_check; ROLLBACK; — это способ «потрогать» изменения и откатить их.

Например, перед опасной миграцией: запустить весь UPDATE, посмотреть, что получилось, и откатить. Если результат правильный — повторить и закоммитить.

Пробный прогон UPDATE через BEGIN/ROLLBACK — посмотреть, не сломали ли:

PostgreSQL

В production-инструментах (например, psql или DBeaver) этот паттерн прокачивается до полноценного workflow: открыл транзакцию, прогнал миграцию, посмотрел EXPLAIN или счётчики, и осознанно — COMMIT или ROLLBACK.

Что нельзя внутри транзакции

Большинство DDL (CREATE, ALTER, DROP TABLE, CREATE INDEX) в PostgreSQL транзакционные — их можно откатить через ROLLBACK. Это редкая фича: в MySQL до недавнего времени DDL был неявно автокоммиченным.

Но есть исключения, которые не работают в транзакции:

  • CREATE INDEX CONCURRENTLY — потому что он работает в фоне, мультистадийно.
  • VACUUM, REINDEX CONCURRENTLY, CLUSTER — административные операции.
  • CREATE DATABASE, DROP DATABASE — менеджмент.
  • ALTER SYSTEM — изменение конфигурации.

Если попробуешь — получишь ошибку «cannot run inside a transaction block». Это редко мешает в обычной работе, но полезно знать.

Что произойдёт после ошибки внутри транзакции

Это критически важное поведение: любая ошибка переводит транзакцию в aborted state. Любая следующая команда вернёт ошибку «current transaction is aborted, commands ignored until end of transaction block». Единственный выход — ROLLBACK. Это отличается, например, от Oracle, где после ошибки можно продолжить.

После ошибки транзакция помечается aborted — нужно ROLLBACK:

PostgreSQL

В реальном приложении это означает: если внутри транзакции произошла ошибка — нужно перехватить её и сделать ROLLBACK, иначе все следующие команды (включая, возможно, попытку записать ошибку в лог-таблицу той же базы) пойдут в стену.

Для частичной обработки ошибок без полного отката есть savepoints — об этом следующий урок.

Проверка знанийKnowledge check
Внутри одной транзакции выполнили 3 UPDATE. После второго случилась ошибка нарушения CHECK. Что произойдёт, если попытаться выполнить третий UPDATE, и что — если COMMIT?
ОтветAnswer
Третий UPDATE вернёт ошибку «current transaction is aborted, commands ignored until end of transaction block» — после первой ошибки транзакция в aborted state, новые команды не принимаются. COMMIT в этом состоянии тоже не сработает: PostgreSQL вернёт NOTICE и фактически выполнит ROLLBACK (это можно увидеть в логах). То есть транзакция гарантированно откатится — нельзя «частично закоммитить» транзакцию после ошибки. Чтобы пережить часть изменений из такой транзакции, нужны SAVEPOINT — следующий урок.

Длинные транзакции — антипаттерн

Транзакция, которая длится секунды или минуты, — почти всегда плохая идея в OLTP. Причины:

  1. Долго держит локи. Все строки, которые транзакция изменила, заблокированы для других писателей до COMMIT. На странице, которая трогает популярную строку, это создаёт очередь.
  2. Блокирует VACUUM. PostgreSQL не может очистить старые версии строк (см. MVCC в уроке 5), пока хоть одна транзакция их видит. Открытая транзакция «фиксирует прошлое», и таблицы начинают раздуваться.
  3. Растёт окно для аномалий. Чем дольше транзакция, тем больше шанс, что параллельные транзакции изменят данные, на которых она строит логику.

Признаки проблемы: запрос SELECT * FROM pg_stat_activity WHERE state != 'idle' AND xact_start < now() - interval '1 minute'; находит транзакции старше минуты. На здоровом сервисе таких быть не должно.

Что считается «нормальной длительностью» транзакции в OLTP: миллисекунды до десятков миллисекунд. Транзакция, которая ждёт ответа от внешнего сервиса по HTTP, — это уже подозрительно: она может зависнуть на секунды.

Хорошо vs плохо: длительность транзакции

Короткая транзакция — счастливые продакшен-метрики. Долгая — табличное раздувание и очереди блокировок.

Хороший паттерн
(prepare data outside)
BEGIN
UPDATE accounts ...
UPDATE accounts ...
INSERT INTO transfers ...
COMMIT~5 ms
(call external service AFTER commit)
Плохой паттерн
BEGIN
UPDATE accounts ...
HTTP POST /externalждём 2 секунды; в это время локи держатся
UPDATE accounts ...
COMMIT~2000 ms — антипаттерн

Правило: внутри транзакции — только работа с базой. Всё внешнее (HTTP, файлы, сообщения) — снаружи. Если нужно сделать «послать email после успешного заказа» — сначала COMMIT, потом email. Не наоборот.

Что происходит с xmin/xmax при BEGIN и COMMIT Управление транзакциями из Python

Чек-лист

  • В PostgreSQL по умолчанию работает autocommit: каждое выражение оборачивается в свою транзакцию.
  • Явная транзакция: BEGIN;COMMIT; или ROLLBACK;.
  • Свои изменения видны внутри транзакции сразу. Чужие — зависит от уровня изоляции (см. урок 5).
  • ROLLBACK — это полное «undo» транзакции. Безопасно использовать для dry-run миграций.
  • DDL (CREATE/ALTER/DROP) в PostgreSQL транзакционный — можно откатить.
  • Исключения: CREATE INDEX CONCURRENTLY, VACUUM, ALTER SYSTEM.
  • Любая ошибка переводит транзакцию в aborted state. Единственный выход — ROLLBACK. Для частичной обработки — savepoints.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое autocommit в PostgreSQL?

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

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

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

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