Глиф Учебники

Урок 10. Транзакции — BEGIN, COMMIT, ROLLBACK

Курс «SQL» · урок 10 из 10 · 4 минуты чтения

Перевод денег с одного счёта на другой — это два изменения: списать у одного, зачислить другому. Если между ними пропадёт свет или программа упадёт, деньги исчезнут: списание прошло, зачисление — нет. Транзакция объединяет несколько команд в одно целое: либо выполняются все, либо ни одной.

Учебная таблица

Для этого урока нужна отдельная таблица счетов:

SQL
CREATE TABLE accounts (
    id      INTEGER PRIMARY KEY,
    owner   TEXT    NOT NULL,
    balance INTEGER NOT NULL CHECK (balance >= 0)
);

INSERT INTO accounts (owner, balance) VALUES ('Анна', 100), ('Борис', 50);

CHECK (balance >= 0) не даст счёту уйти в минус.

BEGIN и COMMIT

Анна переводит Борису 30 рублей:

SQL
BEGIN;
UPDATE accounts SET balance = balance - 30 WHERE id = 1;
UPDATE accounts SET balance = balance + 30 WHERE id = 2;
COMMIT;

SELECT * FROM accounts;
idownerbalance
1Анна70
2Борис80

BEGIN открывает транзакцию. Изменения после него видны в этом же подключении, но в базу окончательно ещё не записаны. COMMIT фиксирует их все разом. Если до COMMIT программа упадёт или пропадёт питание, после перезапуска база окажется в состоянии до BEGIN — как будто перевода не было. Половины перевода не будет никогда.

BEGIN; balance - 30 у Анны, id = 1 balance + 30 у Бориса, id = 2 COMMIT; Анна 100 Борис 50 Анна 70 Борис 50 Анна 70 Борис 80 Анна 70 Борис 80 изменения есть, но в базу ещё не записаны записано разом сбой до COMMIT после перезапуска — как до BEGIN: Анна 100, Борис 50 — половины перевода нет
Перевод в транзакции: после BEGIN оба UPDATE меняют счета, но в базу изменения попадают разом только по COMMIT, а при сбое до COMMIT база возвращается к состоянию до BEGIN.

ROLLBACK — отменить

ROLLBACK отменяет всё, что сделано после BEGIN. Проверьте:

SQL
BEGIN;
DELETE FROM accounts;
SELECT COUNT(*) FROM accounts;
COUNT(*)
0
SQL
ROLLBACK;
SELECT COUNT(*) FROM accounts;
COUNT(*)
2

Счета вернулись. Поэтому изменения рабочей базы удобно делать так: BEGIN, изменение, проверочный SELECT, и только потом COMMIT — или ROLLBACK, если что-то пошло не так.

Ошибка внутри транзакции

Теперь Борис пытается перевести Анне 200 рублей, которых у него нет. Порядок команд неудачный: сначала зачисление, потом списание.

SQL
BEGIN;
UPDATE accounts SET balance = balance + 200 WHERE id = 1;
UPDATE accounts SET balance = balance - 200 WHERE id = 2;

Вторая команда нарушает CHECK и не выполняется: CHECK constraint failed: balance >= 0. Но первая-то уже прошла:

SQL
SELECT * FROM accounts;
idownerbalance
1Анна270
2Борис80

Транзакция всё ещё открыта, и в ней у Анны 270 рублей. Ошибка одной команды в SQLite и MySQL отменяет только эту команду, а не всю транзакцию. Решение принимаете вы — здесь нужен откат:

SQL
ROLLBACK;
SELECT * FROM accounts;
idownerbalance
1Анна70
2Борис80

В программах это правило записывают так: если любая команда транзакции завершилась ошибкой — ROLLBACK, если все прошли — COMMIT.

BEGIN; balance + 200 у Анны balance - 200 у Бориса Анна 70 Борис 80 Анна 270 Борис 80 Анна 270 Борис 80 CHECK constraint failed отменена только эта команда, транзакция открыта ROLLBACK; Анна 70, Борис 80 COMMIT; Анна 270, Борис 80 решаете вы 200 рублей из ниоткуда
В SQLite и MySQL ошибка CHECK отменяет только неудачную команду: транзакция остаётся открытой, у Анны 270, и ROLLBACK вернёт прежние счета, а COMMIT записал бы 200 рублей из ниоткуда.

PostgreSQL строже: после ошибки внутри транзакции он отвергает все следующие команды с сообщением current transaction is aborted, commands ignored until end of transaction block. Выход один — ROLLBACK. Даже COMMIT в такой транзакции сработает как откат.

Автофиксация

А что было во всех прошлых уроках, без BEGIN? Каждая команда сама по себе была транзакцией и фиксировалась сразу после выполнения. Это называется автофиксацией (autocommit), и так по умолчанию работают sqlite3, mysql и psql.

Программы ведут себя по-разному:

Свойства транзакций

Гарантии транзакций обозначают словом ACID:

Отличия СУБД

SQLiteMySQL / MariaDBPostgreSQL
НачатьBEGIN;START TRANSACTION; или BEGIN;BEGIN;
CREATE TABLE, DROP TABLE внутри транзакцииоткатываютсяфиксируют транзакцию сразуоткатываются
После ошибки командытранзакция продолжаетсятранзакция продолжаетсятолько ROLLBACK

В MySQL транзакции работают только с таблицами InnoDB — это тип таблиц по умолчанию. Старые таблицы MyISAM транзакций не поддерживают: ROLLBACK для них ничего не вернёт.

В MySQL команды CREATE, ALTER, DROP и TRUNCATE сами фиксируют открытую транзакцию. ROLLBACK после DROP TABLE таблицу не вернёт, а заодно окажутся зафиксированы и все изменения до неё.

Точки сохранения

Внутри транзакции можно поставить метку и откатиться только до неё:

SQL
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
SAVEPOINT before_bonus;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
ROLLBACK TO before_bonus;
COMMIT;

Из двух изменений зафиксируется первое: второе отменено откатом к метке before_bonus. Точки сохранения есть в SQLite, MySQL и PostgreSQL.

BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 1; Анна: −10 SAVEPOINT before_bonus; метка UPDATE accounts SET balance = balance + 1000 WHERE id = 2; +1000 отменено ROLLBACK TO before_bonus; назад к метке COMMIT; записано: −10
ROLLBACK TO before_bonus отменяет только то, что сделано после метки SAVEPOINT, — прибавку 1000 Борису, а COMMIT записывает оставшееся списание 10 рублей у Анны.

Транзакции и блокировки

Пока транзакция с изменениями открыта, другие подключения не могут менять те же данные (в SQLite — вообще ничего в этом файле): они ждут или получают ошибку. Поэтому транзакции делают короткими: открыли, изменили, зафиксировали. Не оставляйте транзакцию открытой, пока ждёте ответа пользователя. Типичная ошибка SQLite в такой ситуации — database is locked, о ней справка Ошибка database is locked.

Попытка открыть транзакцию внутри открытой даёт в SQLite cannot start a transaction within a transaction, а COMMIT без BEGIN — cannot commit - no transaction is active.

Коротко

Проверьте себя

1. Что сделает ROLLBACK после BEGIN; DELETE FROM accounts;?
2. Внутри транзакции SQLite первая команда прошла, вторая нарушила CHECK. Что с изменениями первой команды?
3. Программа на Python добавила строки в SQLite, но после перезапуска их нет. Вероятная причина?
4. Что произойдёт в MySQL с открытой транзакцией после DROP TABLE?