Представьте, что вы списываете деньги с карты пассажира и в этот же момент создаёте ему бронирование. Если деньги ушли, а бронь не записалась (или наоборот), у вас битые данные и злой клиент. Транзакция в postgresql - это способ сказать базе: вот пачка изменений, примени их все или не применяй ни одного. В этом уроке разберём, что стоит за словом ACID, как работают BEGIN, COMMIT и ROLLBACK, почему psql по умолчанию коммитит каждую команду отдельно, и как откатить часть работы через SAVEPOINT, не теряя остальное. Примеры - на демобазе Авиаперевозки.

Как это работает
Транзакция - это логическая единица работы. Внутри неё может быть один INSERT, а может десяток UPDATE по разным таблицам. Снаружи всё это выглядит как одно неделимое действие: либо мир увидел все изменения сразу, либо не увидел ничего.
Четыре буквы ACID описывают гарантии, которые даёт СУБД. Атомарность (A) - все операции транзакции применяются целиком или откатываются целиком, промежуточного состояния для других нет. Согласованность (C) - транзакция переводит базу из одного корректного состояния в другое, не нарушая ограничений целостности (первичные ключи, внешние ключи, CHECK, NOT NULL). Изоляция (I) - параллельные транзакции не лезут друг другу в недоделанную работу, каждая работает как будто она одна. Долговечность (D) - после COMMIT данные уже не пропадут, даже если сервер сразу выключат из розетки.
За долговечность в PostgreSQL отвечает WAL - журнал предзаписи. Прежде чем менять страницы данных на диске, сервер пишет в журнал запись об изменении и сбрасывает её на диск при коммите. Если питание пропало, при следующем старте PostgreSQL проигрывает журнал и восстанавливает все подтверждённые транзакции. Поэтому COMMIT - это не просто слово, а гарантия, подкреплённая записью на диск.
Изоляцию PostgreSQL обеспечивает через MVCC - многоверсионность. Каждая строка имеет версии, и транзакция видит снимок данных на свой момент. Незакоммиченные чужие изменения вам не видны - грязного чтения тут не бывает в принципе. Глубоко уровни изоляции разберём в следующем уроке, сейчас важно понять: ваша незавершённая транзакция изолирована от остальных.
Теперь про автокоммит. В psql каждый отдельный SQL-оператор по умолчанию выполняется в своей маленькой транзакции и сразу коммитится. Это удобно для одиночных команд, но опасно, когда нужно несколько шагов как одно целое. Чтобы объединить их, вы явно открываете транзакцию командой BEGIN, делаете работу и закрываете её через COMMIT (применить) или ROLLBACK (откатить всё). Между BEGIN и COMMIT автокоммита нет.
SAVEPOINT - это именованная точка внутри транзакции. Сделав сейвпойнт, вы можете откатиться к нему через ROLLBACK TO, отменив только то, что было после, и продолжить транзакцию дальше. Это частичный откат: вся транзакция при этом ещё жива.
Важная особенность PostgreSQL: если внутри транзакции возникла ошибка, вся транзакция переходит в состояние aborted, и любой следующий запрос вернёт ошибку про current transaction is aborted, пока вы не сделаете ROLLBACK или не откатитесь к сейвпойнту. Это частые грабли новичков и боль при миграции postgresql для 1с, где привыкли к более снисходительному поведению других СУБД.
SQL и примеры
Базовый перевод денег между двумя бронями - классический пример атомарности. Либо обе строки изменятся, либо ни одна.
Код: Выделить всё
BEGIN;
UPDATE bookings SET total_amount = total_amount - 10000
WHERE book_ref = '0824C5';
UPDATE bookings SET total_amount = total_amount + 10000
WHERE book_ref = '00044C';
COMMIT;Откат, когда передумали. Проверяем сумму, она нас не устраивает - отменяем все изменения транзакции:
Код: Выделить всё
BEGIN;
UPDATE ticket_flights SET amount = amount * 1.10
WHERE flight_id = 1;
-- посмотрели, поняли что подняли цену не тому рейсу
ROLLBACK;Частичный откат через SAVEPOINT. Создаём бронь, потом два билета, но второй билет ошибочный - откатываем только его:
Код: Выделить всё
BEGIN;
INSERT INTO bookings (book_ref, book_date, total_amount)
VALUES ('FF00AA', now(), 0);
INSERT INTO tickets (ticket_no, book_ref, passenger_id, passenger_name)
VALUES ('0005550000001', 'FF00AA', '1234 567890', 'IVAN PETROV');
SAVEPOINT before_second;
INSERT INTO tickets (ticket_no, book_ref, passenger_id, passenger_name)
VALUES ('0005550000001', 'FF00AA', '0000 000000', 'DUP'); -- дубль ticket_no, ошибка
ROLLBACK TO before_second; -- отменили только проблемный билет
COMMIT; -- бронь и первый билет сохраненыПосмотреть свой текущий идентификатор транзакции и убедиться, что вы внутри неё:
Код: Выделить всё
BEGIN;
SELECT pg_current_xact_id(); -- в PG 13+; раньше txid_current()
COMMIT;Частые грабли
- Забыли COMMIT и ушли с открытой транзакцией - она держит блокировки и старые версии строк, мешает другим и раздувает работу для vacuum. Долгая открытая транзакция - враг здоровья базы.
- Ошибка внутри BEGIN переводит транзакцию в aborted, и все следующие команды падают с current transaction is aborted. Лечится ROLLBACK или ROLLBACK TO savepoint, а не повтором запроса.
- Думают, что ROLLBACK откатит уже закоммиченную транзакцию. Нет: после COMMIT обратной дороги нет, только новая компенсирующая транзакция.
- Полагаются на автокоммит psql там, где нужна атомарность нескольких шагов. Многошаговую логику всегда оборачивайте в BEGIN/COMMIT.
- Внутри транзакции выполнили TRUNCATE или DDL и решили, что это нельзя откатить. В PostgreSQL почти весь DDL транзакционный и откатывается ROLLBACK - приятное отличие от ряда других СУБД.
- Путают ROLLBACK (откат всей транзакции) и ROLLBACK TO savepoint (частичный откат). Второй не закрывает транзакцию.
- Подключитесь к демобазе: psql -d demo. Проверьте режим автокоммита командой \echo :AUTOCOMMIT (в psql он включён по умолчанию).
- Откройте транзакцию: BEGIN; и выполните UPDATE bookings SET total_amount = total_amount + 1 WHERE book_ref = (SELECT book_ref FROM bookings LIMIT 1);
- В другом терминале откройте второй psql и сделайте SELECT той же строки - убедитесь, что изменение пока не видно (изоляция).
- Вернитесь в первый сеанс, сделайте SAVEPOINT sp1; и ещё один UPDATE, затем ROLLBACK TO sp1; - проверьте, что второй UPDATE отменён, а первый жив.
- Сделайте намеренную ошибку (вставьте строку с дублирующимся ключом), убедитесь, что транзакция перешла в aborted и следующий SELECT падает.
- Выполните ROLLBACK; - вся транзакция отменена. Проверьте в обоих сеансах, что total_amount вернулся к исходному.
- Повторите сценарий, но завершите COMMIT вместо ROLLBACK, и убедитесь, что изменение теперь видно второму сеансу.
- Что означает каждая буква в ACID и какой компонент PostgreSQL отвечает за долговечность?
- Чем отличается поведение psql с автокоммитом от работы внутри явного BEGIN/COMMIT?
- Что произойдёт с транзакцией после ошибки в одном из её запросов и как из этого состояния выйти?
- В чём разница между ROLLBACK и ROLLBACK TO SAVEPOINT?
- Почему долго открытая транзакция вредна для базы и при чём тут vacuum?
- Можно ли в PostgreSQL откатить DDL-команду (например CREATE TABLE), выполненную внутри транзакции?