Транзакции
Предпосылки: DML (INSERT, UPDATE, DELETE), ACID.
← DML — изменение данных | Составные DML-операции →
Перевод денег между счетами: списание с одного и зачисление на другой. Если между двумя UPDATE произойдёт сбой — деньги списаны, но не зачислены. Транзакция решает эту проблему: группа операций выполняется целиком или не выполняется вообще.
BEGIN, COMMIT, ROLLBACK
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;BEGIN (англ. «начать») открывает транзакцию. COMMIT (англ. «зафиксировать, подтвердить») фиксирует все изменения. До COMMIT изменения видны только текущей транзакции.
Если что-то пошло не так:
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- ошибка обнаружена
ROLLBACK;ROLLBACK (англ. «откатить назад») отменяет все изменения с момента BEGIN. Таблица остаётся в состоянии до BEGIN.
Автоматические транзакции
Каждая отдельная команда SQL, выполненная без явного BEGIN, автоматически оборачивается в транзакцию:
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- эквивалентно:
-- BEGIN;
-- UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- COMMIT;Это значит: одна команда — атомарна даже без явного BEGIN. Явный BEGIN нужен, когда несколько команд должны быть атомарными.
SAVEPOINT — частичный откат
SAVEPOINT (англ. «точка сохранения») создаёт контрольную точку внутри транзакции. ROLLBACK TO откатывает к savepoint, но не закрывает транзакцию — после отката можно продолжить работу.
Импорт каталога товаров из CSV: 10 000 строк, часть содержит невалидные данные (дубли SKU, нарушение CHECK). Без SAVEPOINT одна ошибка откатывает весь BEGIN — 9 999 валидных строк потеряны. С SAVEPOINT перед каждым батчем ошибочный батч откатывается, а остальные фиксируются:
BEGIN;
SAVEPOINT batch_1;
INSERT INTO products (sku, name, price) VALUES
('A001', 'Keyboard', 2500),
('A002', 'Mouse', 1200);
-- OK, продолжаем
SAVEPOINT batch_2;
INSERT INTO products (sku, name, price) VALUES
('A001', 'Duplicate', 999); -- нарушение UNIQUE(sku)
ROLLBACK TO batch_2;
-- batch_1 сохранён, batch_2 отменён
SAVEPOINT batch_3;
INSERT INTO products (sku, name, price) VALUES
('A003', 'Monitor', 35000);
-- OK
COMMIT; -- зафиксированы batch_1 и batch_3ROLLBACK TO уничтожает все savepoint’ы, созданные после указанного. Если есть sp1 → sp2 → sp3 и выполняется ROLLBACK TO sp1, то sp2 и sp3 исчезают — откатить к ним уже нельзя. Для продолжения работы после отката нужно создать новый savepoint.
Savepoint’ы не бесплатны: СУБД отслеживает состояние каждого, и на масштабе сотен тысяч overhead становится заметным. В PostgreSQL каждый SAVEPOINT создаёт subtransaction state — подробнее в паттернах параллельного доступа.
Долгие транзакции
Разработчик открыл BEGIN, выполнил SELECT для отладки и ушёл на обед. Пока транзакция открыта, СУБД не может освободить ресурсы, связанные с изменёнными строками — даже если все остальные транзакции давно завершились. Результат: рост занимаемого места и замедление запросов.
Та же проблема возникает в приложении: HTTP-запрос открывает транзакцию, делает внешний API-вызов (2 секунды timeout), потом продолжает работу с базой. Всё время ожидания API транзакция удерживает ресурсы.
В PostgreSQL конкретный механизм — MVCC: открытая транзакция не позволяет СУБД освободить старые версии строк, и таблица может вырасти на гигабайты. Защита — idle_in_transaction_session_timeout.
ACID
Транзакции обеспечивают четыре свойства, описанные в ACID:
Atomicity (атомарность) — транзакция выполняется целиком или не выполняется. Частичного результата нет.
Consistency (согласованность) — транзакция переводит базу из одного корректного состояния в другое. Ограничения (constraints) проверяются в конце транзакции.
Isolation (изоляция) — параллельные транзакции не видят незафиксированных изменений друг друга (степень изоляции зависит от уровня).
Durability (устойчивость) — после COMMIT данные сохранены, даже при сбое.
Уровни изоляции
Параллельные транзакции могут мешать друг другу: одна читает данные, которые другая ещё не зафиксировала, или повторный SELECT возвращает другой результат, потому что между двумя чтениями другая транзакция изменила строку. Стандарт SQL определяет классы таких помех (аномалий) и уровни изоляции, блокирующие их. Чем выше уровень — тем больше аномалий предотвращено, но тем дороже обходятся параллельные операции.
Аномалии
Dirty read (англ. «грязное чтение») — транзакция видит незафиксированные изменения другой транзакции. Если та откатится, прочитанные данные никогда не существовали.
Non-repeatable read (англ. «неповторяемое чтение») — повторный SELECT той же строки возвращает другое значение, потому что между двумя чтениями другая транзакция изменила и зафиксировала строку.
Phantom read (англ. «фантомное чтение») — повторный SELECT по тому же WHERE возвращает другой набор строк, потому что другая транзакция вставила или удалила строки, попадающие под условие.
Serialization anomaly (аномалия сериализации) — результат параллельного выполнения транзакций невозможно получить при любом последовательном порядке. Пример: две транзакции одновременно читают сумму и увеличивают её на 100 — обе прибавляют к старому значению, вместо того чтобы одна увидела результат другой.
Уровни изоляции SQL
| Уровень | Dirty read | Non-repeatable read | Phantom read | Serialization anomaly |
|---|---|---|---|---|
| READ UNCOMMITTED | возможен | возможен | возможен | возможен |
| READ COMMITTED | нет | возможен | возможен | возможен |
| REPEATABLE READ | нет | нет | возможен* | возможен |
| SERIALIZABLE | нет | нет | нет | нет |
* Стандарт SQL допускает phantom read на уровне REPEATABLE READ. PostgreSQL реализует RR через snapshot isolation, что фактически предотвращает и phantom read — транзакция видит только строки, существовавшие на момент её первого запроса.
READ COMMITTED — уровень по умолчанию в PostgreSQL и Oracle. Каждый оператор видит данные, зафиксированные к моменту его начала. REPEATABLE READ фиксирует snapshot на первом запросе транзакции — все SELECT видят одни и те же данные:
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- ... другая транзакция меняет balance на 500 и COMMIT ...
SELECT balance FROM accounts WHERE id = 1; -- 1000 (тот же snapshot)
COMMIT;SERIALIZABLE — самый строгий уровень: СУБД гарантирует, что результат параллельного выполнения эквивалентен какому-то последовательному порядку. Цена: транзакция может получить ошибку сериализации и должна быть перезапущена.
Как PostgreSQL реализует эти уровни через версионирование строк — в уровнях изоляции PostgreSQL. Типовые паттерны работы с конкурентным доступом (SELECT FOR UPDATE, advisory locks, retry loops) — в паттернах параллельного доступа.
Sources
- PostgreSQL Documentation (v16): Transaction Management. https://www.postgresql.org/docs/16/tutorial-transactions.html
- PostgreSQL Documentation (v16): SET TRANSACTION. https://www.postgresql.org/docs/16/sql-set-transaction.html