Транзакции: 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 и уровни изоляции: что происходит, когда несколько транзакций выполняются одновременно.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике