В PostgreSQL транзакции всегда
Это удивляет новичков: в PostgreSQL каждая команда — это уже транзакция. Когда ты пишешь SELECT * FROM customers; — за кулисами происходит:
BEGIN(неявный).- Сам
SELECT. COMMIT(неявный).
Это режим называется
BEGIN (или эквивалентным START TRANSACTION) и закрывается одной из двух команд: COMMIT фиксирует изменения, ROLLBACK откатывает.
Внутри явной транзакции тоже всё транзакционно: каждый отдельный UPDATE либо применяется ко всем выбранным строкам, либо ни к одной. Но все эти отдельные UPDATE остаются «в воздухе» до общего COMMIT — для других сессий они невидимы.
Слева: каждая команда — своя транзакция. Справа: одна транзакция вокруг трёх команд, commit один.
BEGIN, COMMIT, ROLLBACK
Синтаксис простой:
BEGIN;— открыть транзакцию. В стандарте SQL официальноSTART TRANSACTION, но PostgreSQL принимает оба варианта.COMMIT;— зафиксировать изменения. ПослеCOMMITони видны всем остальным сессиям и переживут падение базы.ROLLBACK;— отменить транзакцию. Все изменения внутри неё откатываются, как будто их не было.
Если в открытой транзакции ты разорвал соединение (например, упал клиент) — PostgreSQL автоматически делает ROLLBACK. Незакоммиченное никогда не сохраняется.
Простая транзакция с COMMIT — изменения остаются:
Та же транзакция, но с ROLLBACK — изменения откатываются:
Заметь интересную асимметрию: внутри транзакции ты видишь свои собственные изменения, как будто они уже применены. Снаружи (другие сессии) их не видят до COMMIT. Это поведение называется
Что видно внутри транзакции — а что нет
Это ключевой момент, который часто путают.
Свои изменения видны сразу. Если ты в одной сессии сделал UPDATE, следующий SELECT в этой же сессии (даже без commit) увидит обновлённые данные.
Чужие изменения — зависит от уровня изоляции. На дефолтном READ COMMITTED ты видишь чужие commit’нутые изменения сразу. На REPEATABLE READ — только те, что были commit’нуты до начала твоей транзакции. Об этом — урок 5.
Незакоммиченные чужие изменения — никогда. PostgreSQL вообще не имеет уровня READ UNCOMMITTED, на котором были бы видны чужие непакоммиченные данные. То есть «dirty read» (грязное чтение) в PostgreSQL невозможно по умолчанию, на любом уровне.
Внутри транзакции ты видишь свои собственные изменения, как будто они уже применились:
Когда нужна явная транзакция
Главное правило: группа связанных изменений = одна транзакция. Если есть бизнес-инвариант, который должен сохраняться между несколькими UPDATE/INSERT/DELETE — оберни всё в BEGIN...COMMIT.
Типичные случаи:
- Перевод между сущностями. Списать с одного аккаунта, зачислить на другой. Либо обе операции, либо ни одной.
- Создание заказа.
INSERT INTO orders, потомINSERT INTO order_itemsнесколько раз, потомUPDATE products SET in_stock = in_stock - qty. Все вместе. - Миграции схемы.
ALTER TABLE,UPDATEдля backfill,ALTER TABLEдля NOT NULL. Если что-то упало — нужно вернуться к старой схеме целиком. - Импорт данных. Если импортируешь 100 000 строк и на 99 999 случилась ошибка — должно ли остаться 99 998 строк? Обычно нет.
Реалистичная транзакция: создаём заказ и резервируем товар одной транзакцией:
Что было бы без транзакции: между 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 — посмотреть, не сломали ли:
В 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:
В реальном приложении это означает: если внутри транзакции произошла ошибка — нужно перехватить её и сделать ROLLBACK, иначе все следующие команды (включая, возможно, попытку записать ошибку в лог-таблицу той же базы) пойдут в стену.
Для частичной обработки ошибок без полного отката есть savepoints — об этом следующий урок.
Длинные транзакции — антипаттерн
Транзакция, которая длится секунды или минуты, — почти всегда плохая идея в OLTP. Причины:
- Долго держит локи. Все строки, которые транзакция изменила, заблокированы для других писателей до COMMIT. На странице, которая трогает популярную строку, это создаёт очередь.
- Блокирует VACUUM. PostgreSQL не может очистить старые версии строк (см. MVCC в уроке 5), пока хоть одна транзакция их видит. Открытая транзакция «фиксирует прошлое», и таблицы начинают раздуваться.
- Растёт окно для аномалий. Чем дольше транзакция, тем больше шанс, что параллельные транзакции изменят данные, на которых она строит логику.
Признаки проблемы: запрос SELECT * FROM pg_stat_activity WHERE state != 'idle' AND xact_start < now() - interval '1 minute'; находит транзакции старше минуты. На здоровом сервисе таких быть не должно.
Что считается «нормальной длительностью» транзакции в OLTP: миллисекунды до десятков миллисекунд. Транзакция, которая ждёт ответа от внешнего сервиса по HTTP, — это уже подозрительно: она может зависнуть на секунды.
Короткая транзакция — счастливые продакшен-метрики. Долгая — табличное раздувание и очереди блокировок.
Правило: внутри транзакции — только работа с базой. Всё внешнее (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.