Транзакции: BEGIN, COMMIT, ROLLBACK
Транзакции: BEGIN, COMMIT, ROLLBACK
Предположим, вы переводите деньги с одного счёта на другой. Это два шага: списать с одного, зачислить на другой. Если после первого шага система упадёт — деньги исчезнут: счёт уменьшился, но на другой ничего не поступило. Транзакция решает эту проблему: она гарантирует, что либо оба шага выполняются, либо ни один. В SQL транзакция — группа команд, которая выполняется как единое целое.
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 фиксирует всё: изменения становятся постоянными и видимыми всем.
Если что-то пошло не так — ROLLBACK откатывает все изменения транзакции:
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- Проверяем баланс:
SELECT balance FROM accounts WHERE id = 1;
-- Отрицательный! Недопустимо.
ROLLBACK; -- откат: баланс вернётся к исходному значению
После ROLLBACK база данных возвращается в состояние, которое было до BEGIN. Ни одно из изменений транзакции не применяется.
Автокоммит (без явного BEGIN)
Если не открывать явную транзакцию, каждая команда автоматически выполняется в собственной мини-транзакции:
-- Это эквивалентно:
BEGIN;
UPDATE products SET price = price * 1.1 WHERE category_id = 5;
COMMIT;
-- и записывается как:
UPDATE products SET price = price * 1.1 WHERE category_id = 5;
-- Автоматически коммитится после выполнения
Режим автокоммита удобен для отдельных запросов, но опасен для операций, требующих нескольких шагов. Банковский перевод в режиме автокоммита не безопасен — если второй UPDATE упадёт, первый уже зафиксирован.
SAVEPOINT: вложенная точка сохранения
Внутри транзакции можно создать точку сохранения и откатиться к ней, не откатывая всю транзакцию:
BEGIN;
INSERT INTO orders (customer_id, total) VALUES (42, 1500);
SAVEPOINT after_order; -- точка сохранения
INSERT INTO order_items (order_id, product_id, quantity) VALUES (LASTVAL(), 99, 2);
-- Что-то пошло не так с позицией заказа:
ROLLBACK TO SAVEPOINT after_order; -- откат к точке, заказ сохранён
-- Попробовать другую позицию:
INSERT INTO order_items (order_id, product_id, quantity) VALUES (LASTVAL(), 100, 1);
COMMIT; -- фиксируем: заказ + исправленная позиция
SAVEPOINT позволяет обрабатывать ошибки внутри транзакции точечно, не теряя всю выполненную работу. После ROLLBACK TO SAVEPOINT транзакция продолжается — не завершается.
Обработка ошибок в транзакции
В PostgreSQL, если команда внутри транзакции вызывает ошибку, транзакция переходит в состояние «aborted» — до явного ROLLBACK никакие команды не выполняются:
BEGIN;
INSERT INTO users (email) VALUES ('anna@example.com');
INSERT INTO users (email) VALUES ('anna@example.com'); -- Дублирующий email!
-- ERROR: duplicate key value violates unique constraint "users_email_key"
-- После ошибки транзакция в состоянии aborted:
INSERT INTO users (email) VALUES ('boris@example.com');
-- ERROR: current transaction is aborted, commands ignored until end of transaction block
ROLLBACK; -- только после этого можно начать новую транзакцию
Это защита: PostgreSQL не позволяет продолжать работу в сломанной транзакции. После ROLLBACK можно открыть новую и повторить операцию с исправленными данными.
Транзакции и DDL
PostgreSQL — редкая СУБД, где DDL-команды (CREATE TABLE, ALTER TABLE, DROP TABLE) тоже транзакционны:
BEGIN;
CREATE TABLE temp_migration (id SERIAL, data TEXT);
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
-- Что-то пошло не так:
ROLLBACK;
-- Таблица temp_migration не создана, колонка last_login не добавлена
Это делает миграции схемы безопасными: если при применении набора изменений одно упало — всё можно откатить. В MySQL и SQL Server DDL делает неявный COMMIT, откат невозможен.
Длинные транзакции: почему это опасно
Транзакция «живёт» от BEGIN до COMMIT или ROLLBACK. Чем длиннее транзакция — тем дольше она удерживает блокировки и версии строк. Это создаёт проблемы:
Bloat (раздувание таблиц): PostgreSQL использует MVCC — не удаляет старые версии строк физически, пока есть транзакции, которые могут их видеть. Длинная транзакция удерживает старые версии, не давая процессу autovacuum их убрать. Таблицы «раздуваются», запросы замедляются.
Накопление блокировок: чем дольше транзакция, тем больше строк она может заблокировать, создавая очереди ожидания для других транзакций.
Практические правила:
- Не держать транзакцию открытой во время ожидания пользовательского ввода
- Не смешивать длинные аналитические запросы с транзакциями изменения данных
- Явно завершать транзакцию в
finally-блоке приложения (даже при ошибке)
-- Плохо: транзакция открыта во время HTTP-запроса к внешнему API
-- (приложение может ждать секунды)
BEGIN;
UPDATE orders SET status = 'processing' WHERE id = 42;
-- ... вызов внешнего API (может занять секунды) ...
COMMIT;
-- Лучше: минимизировать время между BEGIN и COMMIT
-- Вынести внешние вызовы за пределы транзакции
Блокировки в транзакциях
Когда транзакция изменяет строку, PostgreSQL блокирует её для других транзакций, которые хотят тоже изменить эту строку. Другие транзакции ждут:
-- Сессия 1:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- (не коммитит)
-- Сессия 2:
BEGIN;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- Ждёт... пока сессия 1 не сделает COMMIT или ROLLBACK
Чтение (SELECT) не блокирует запись — это основа MVCC (многоверсионного управления параллельным доступом). Подробнее — в следующем уроке про ACID.
Deadlock (взаимная блокировка) — ситуация когда две транзакции ждут друг друга:
-- Сессия 1: заблокировала строку id=1, ждёт id=2
-- Сессия 2: заблокировала строку id=2, ждёт id=1
-- Deadlock: PostgreSQL обнаружит и прервёт одну из транзакций с ошибкой
PostgreSQL автоматически обнаруживает deadlock и откатывает одну из транзакций. Для профилактики: всегда блокируйте ресурсы в одном порядке во всём приложении.
SELECT FOR UPDATE: явная блокировка строки
Иногда нужно заблокировать строку для чтения, чтобы никто другой не изменил её до конца транзакции:
BEGIN;
-- Прочитать баланс и заблокировать строку
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Теперь другая транзакция не может изменить эту строку до нашего COMMIT/ROLLBACK
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
FOR UPDATE — пессимистическая блокировка. Используется, когда нельзя допустить параллельного изменения между чтением и записью.
Просмотр активных транзакций
Диагностика: увидеть транзакции, которые выполняются прямо сейчас:
-- Активные запросы и их транзакции
SELECT pid, usename, state, query_start,
now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
Транзакция с большим duration в состоянии idle in transaction — открытая транзакция без активного запроса. Это проблема: приложение забыло сделать COMMIT. Такую транзакцию можно прервать:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - query_start > INTERVAL '10 minutes';
Практический пример: безопасный перевод
BEGIN;
-- Заблокировать оба счёта в одном порядке (профилактика deadlock)
SELECT balance FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- Проверить достаточность баланса
DO $$
DECLARE
v_balance NUMERIC;
BEGIN
SELECT balance INTO v_balance FROM accounts WHERE id = 1;
IF v_balance < 1000 THEN
RAISE EXCEPTION 'Insufficient balance';
END IF;
END $$;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
INSERT INTO transfer_log (from_id, to_id, amount, created_at)
VALUES (1, 2, 1000, NOW());
COMMIT;
Краткий итог
BEGIN/START TRANSACTION— начать транзакциюCOMMIT— зафиксировать все измененияROLLBACK— откатить все изменения транзакцииSAVEPOINT name— промежуточная точка;ROLLBACK TO SAVEPOINT name— частичный откат- Без явного
BEGINкаждая команда — автоматическая мини-транзакция - DDL транзакционен в PostgreSQL — миграции безопасны
- После ошибки в транзакции нужен
ROLLBACKперед новой работой SELECT FOR UPDATE— явная блокировка строки для чтения с намерением обновить
Что дальше
Транзакции работают — но как именно? Следующий урок раскрывает принципы ACID и уровни изоляции: что происходит, когда несколько транзакций выполняются одновременно.