Транзакции в SQL — BEGIN, COMMIT, ROLLBACK

Коротко

Транзакция — группа SQL-команд, которые выполняются как единое целое: либо все, либо ни одной. BEGIN открывает транзакцию, COMMIT подтверждает, ROLLBACK отменяет. Транзакции защищают данные от частичного обновления и ошибок. Аналитику транзакции нужны при INSERT/UPDATE/DELETE: проверить результат перед подтверждением. На собеседованиях спрашивают ACID, уровни изоляции и deadlock.

Базовый синтаксис

BEGIN;                    -- начало транзакции

UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 2;

COMMIT;                   -- подтвердить оба UPDATE
-- или
ROLLBACK;                 -- отменить оба UPDATE

Классический пример — перевод денег. Без транзакции: если первый UPDATE выполнился, а второй упал (сбой сети, ошибка), деньги списались, но не зачислились. С транзакцией: либо оба UPDATE применяются, либо оба отменяются.

ACID

Транзакции гарантируют четыре свойства — ACID:

Свойство Описание Пример
Atomicity (атомарность) Всё или ничего Перевод: оба UPDATE или ни одного
Consistency (согласованность) Данные остаются корректными Сумма балансов не меняется
Isolation (изолированность) Параллельные транзакции не мешают Два перевода одновременно не теряют деньги
Durability (долговечность) После COMMIT данные сохранены Даже если сервер упадёт

Практические примеры

Безопасный UPDATE

BEGIN;

-- Проверить, что затронет
SELECT COUNT(*) FROM users WHERE last_login < '2023-01-01';
-- 1847 строк

-- Выполнить обновление
UPDATE users
SET status = 'inactive'
WHERE last_login < '2023-01-01';
-- UPDATE 1847

-- Проверить результат
SELECT status, COUNT(*) FROM users GROUP BY status;

-- Если всё ок:
COMMIT;
-- Если что-то не так:
-- ROLLBACK;

Паттерн «BEGIN → проверка → действие → проверка → COMMIT/ROLLBACK» — стандарт для любых массовых изменений данных.

Безопасный DELETE

BEGIN;

-- Сколько удалим?
SELECT COUNT(*) FROM orders WHERE order_date < '2020-01-01';
-- 12543

-- Удаляем
DELETE FROM orders WHERE order_date < '2020-01-01';

-- Проверяем
SELECT MIN(order_date) FROM orders;
-- 2020-01-01 — всё верно

COMMIT;

SAVEPOINT — частичный откат

BEGIN;

INSERT INTO users (name, email) VALUES ('Иван', 'ivan@example.com');

SAVEPOINT sp1;

INSERT INTO users (name, email) VALUES ('Анна', 'INVALID_EMAIL');
-- Ошибка! Откатываем только до savepoint

ROLLBACK TO sp1;

-- Иван всё ещё в транзакции
INSERT INTO users (name, email) VALUES ('Анна', 'anna@example.com');

COMMIT;
-- Оба пользователя сохранены

SAVEPOINT создаёт точку сохранения внутри транзакции. ROLLBACK TO откатывает до этой точки, не отменяя всю транзакцию.

Уровни изоляции

Когда несколько транзакций работают одновременно, возникают проблемы:

Проблема Описание
Dirty read Чтение незакоммиченных данных другой транзакции
Non-repeatable read Повторное чтение даёт другой результат
Phantom read Появляются новые строки при повторном SELECT

Уровни изоляции от слабого к сильному:

Уровень Dirty read Non-repeatable Phantom
READ UNCOMMITTED Да Да Да
READ COMMITTED Нет Да Да
REPEATABLE READ Нет Нет Да
SERIALIZABLE Нет Нет Нет
-- Установить уровень изоляции (PostgreSQL)
BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT SUM(balance) FROM accounts;
-- Другие транзакции не изменят видимые данные до COMMIT

COMMIT;

PostgreSQL по умолчанию — READ COMMITTED. MySQL InnoDB — REPEATABLE READ.

Автокоммит

В большинстве СУБД каждый отдельный запрос — отдельная транзакция:

-- Это автоматически обёрнуто в BEGIN...COMMIT
UPDATE users SET name = 'Иван' WHERE user_id = 1;
-- Изменение применено сразу, ROLLBACK невозможен

Явный BEGIN отключает автокоммит до COMMIT или ROLLBACK.

Deadlock

Deadlock — взаимная блокировка двух транзакций:

Транзакция 1: блокирует строку A, ждёт строку B
Транзакция 2: блокирует строку B, ждёт строку A
→ Обе ждут бесконечно

СУБД обнаруживает deadlock и откатывает одну из транзакций. Защита: всегда блокировать строки в одинаковом порядке (например, по возрастанию ID).

Транзакции для аналитика

Аналитик обычно не пишет сложные транзакции, но использует их для:

  1. Безопасные UPDATE/DELETE — проверить результат перед COMMIT
  2. Скрипты миграции — несколько ALTER TABLE / INSERT в одной транзакции
  3. Заполнение витрин — DROP + CREATE + INSERT как атомарная операция
  4. Понимание на собеседовании — ACID и изоляция спрашивают часто
-- Пересоздание витрины атомарно
BEGIN;

DROP TABLE IF EXISTS daily_metrics;

CREATE TABLE daily_metrics AS
SELECT
    DATE(created_at) AS dt,
    COUNT(DISTINCT user_id) AS dau,
    SUM(amount) AS revenue
FROM events
GROUP BY DATE(created_at);

COMMIT;

Типичные ошибки

Забыли COMMIT. Транзакция без COMMIT блокирует строки. Другие запросы будут ждать. Всегда завершайте BEGIN явным COMMIT или ROLLBACK.

Слишком длинная транзакция. BEGIN → сложный SELECT на 10 минут → UPDATE → COMMIT. Всё это время строки заблокированы. Держите транзакции короткими.

ROLLBACK после COMMIT. Невозможно. COMMIT — необратим. Единственный способ «отменить» — написать обратный UPDATE/DELETE.

Вопросы с собеседований

-- Что такое ACID? -- Atomicity (всё или ничего), Consistency (данные корректны), Isolation (транзакции не мешают друг другу), Durability (после COMMIT данные сохранены навсегда).

-- Чем COMMIT отличается от ROLLBACK? -- COMMIT подтверждает все изменения транзакции. ROLLBACK отменяет все изменения. После COMMIT откатить нельзя.

-- Что такое deadlock? -- Взаимная блокировка: транзакция A ждёт ресурс транзакции B, а B ждёт ресурс A. СУБД обнаруживает и откатывает одну из них.

-- Какой уровень изоляции по умолчанию в PostgreSQL? -- READ COMMITTED. Каждый SELECT видит только закоммиченные данные, но повторный SELECT может дать другой результат (non-repeatable read).


Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.

FAQ

Нужно ли аналитику знать транзакции глубоко?

Базово — да: BEGIN/COMMIT/ROLLBACK для безопасных UPDATE/DELETE и понимание ACID. Уровни изоляции и deadlock — для собеседований и понимания, почему запрос «зависает».

Транзакции в ClickHouse?

ClickHouse не поддерживает классические транзакции. Он оптимизирован для аналитических запросов (OLAP), не для транзакционной нагрузки (OLTP). Для транзакций — PostgreSQL, MySQL.

Как откатить COMMIT?

Никак. COMMIT — необратим. Можно только написать обратную операцию (UPDATE обратно, INSERT → DELETE). Поэтому всегда проверяйте результат перед COMMIT.

Как тренироваться

Транзакции и ACID — обязательная тема на SQL-собеседованиях. Задачи по SQL — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.