SQLтранзакцииACIDбазы данных

Транзакции и ACID в SQL простыми словами

2026-07-11 13 мин

Транзакция — это группа SQL-запросов, которую база данных выполняет как единое целое: либо применяются все изменения разом (COMMIT), либо не применяется ни одно (ROLLBACK). ACID — четыре свойства, которые гарантируют, что даже при сбое сервера, параллельных запросах и обрыве сети данные не превратятся в кашу. Разберём по-человечески, на переводе денег между счетами, и я покажу, почему аналитику это нужно не меньше, чем бэкендеру.

Что такое транзакция и зачем нужны BEGIN, COMMIT и ROLLBACK?

Представь классику: переводим 1000 рублей со счёта A на счёт B. Это два действия — списать у A и зачислить B:

UPDATE accounts SET balance = balance - 1000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 1000 WHERE id = 'B';

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

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 1000 WHERE id = 'B';
COMMIT;
BEGIN (в PostgreSQL — BEGIN или START TRANSACTION) открывает транзакцию. COMMIT фиксирует все изменения — только после него они становятся видны другим и переживут перезагрузку. ROLLBACK отменяет всё, что было после BEGIN, как будто ничего не происходило.

На практике ROLLBACK часто делает приложение автоматически: если внутри блока вылетела ошибка (например, проверка balance >= 0 не прошла), код ловит исключение и откатывает транзакцию. Ты получаешь либо оба UPDATE, либо ноль. Промежуточного состояния «списали, но не зачислили» не существует.

Кстати, тот же BEGIN ... ROLLBACK — мой любимый способ проверять опасный UPDATE или DELETE на проде. Пишешь запрос, смотришь SELECT-ом что зацепилось, и если что-то не так — откатываешь. Потренироваться на живых таблицах без риска можно в SQL-тренажёре — там настоящий PostgreSQL прямо в браузере.

Что означает ACID на примере перевода денег?

ACID — это аббревиатура из четырёх свойств: Atomicity, Consistency, Isolation, Durability. Прогоним каждое через наш перевод денег.

Atomicity (атомарность) — «всё или ничего». Оба UPDATE применяются вместе, либо не применяется ни один. Никакого «списали у A, а на B не успели». Именно это мы разобрали выше.

Consistency (согласованность) — база переходит из одного корректного состояния в другое, не нарушая правил. Если на счетах стоит ограничение CHECK (balance >= 0), а на A всего 500 рублей, попытка списать 1000 нарушит правило, и вся транзакция откатится. Сумма денег в системе до и после перевода одинаковая — это тоже согласованность.

Isolation (изолированность) — параллельные транзакции не видят «полуфабрикатов» друг друга. Пока перевод не зафиксирован, другой запрос не должен увидеть, что у A уже списалось, а B ещё не пополнился. Как именно это работает — зависит от уровня изоляции, и это самая интересная часть для аналитика.

Durability (долговечность) — после COMMIT данные записаны надёжно. Даже если через секунду выдернуть шнур из сервера, зафиксированный перевод никуда не денется — СУБД пишет изменения в журнал (WAL) до подтверждения.

Первые две буквы (A и C) для аналитика чаще всего «просто работают» на стороне разработки. А вот I — изоляция — напрямую влияет на то, что ты увидишь в своих выгрузках. К ней и переходим.

Зачем аналитику вообще знать про уровни изоляции?

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

Пример из жизни. Ты считаешь дневную выручку тяжёлым запросом с несколькими JOIN и подзапросами. Запрос идёт 40 секунд. За эти 40 секунд прошло 200 новых оплат. Вопрос: попадут ли они в результат частично, целиком или вообще нет? От ответа зависит, сойдётся ли твоя цифра с дашбордом и не придётся ли объяснять продакту, почему выручка «прыгает» при повторном прогоне того же запроса.

Второй момент — блокировки. Аналитический SELECT обычно никого не блокирует, но если ты в транзакции держишь открытым долгий запрос или, не дай бог, делаешь UPDATE на большой таблице, ты можешь встать поперёк продовых процессов. Понимание изоляции помогает не положить прод одним неаккуратным запросом.

Если хочется системно прокачать SQL под такие ситуации, а не только транзакции — загляни в курс по SQL с нуля, а точечные конструкции всегда можно быстро подсмотреть в SQL-справочнике.

Какие бывают аномалии: dirty read, non-repeatable read и phantom read?

Прежде чем говорить про уровни, нужно понять, от каких трёх бед они защищают. Все три — про то, что видит транзакция, когда рядом работают другие.

Dirty read (грязное чтение) — ты читаешь данные, которые другая транзакция ещё не зафиксировала. Она сделала UPDATE, ты его увидел, а потом она откатилась через ROLLBACK. Получается, ты прочитал значение, которого официально никогда не существовало. Для отчёта по деньгам это катастрофа: посчитаешь выручку по платежу, который отменился.

Non-repeatable read (неповторяемое чтение) — ты дважды читаешь одну и ту же строку внутри своей транзакции и получаешь разные значения, потому что между твоими чтениями другая транзакция успела эту строку обновить и закоммитить.

BEGIN;
SELECT balance FROM accounts WHERE id = 'A';  -- 5000
-- в этот момент другая транзакция сделала UPDATE и COMMIT
SELECT balance FROM accounts WHERE id = 'A';  -- уже 4000
COMMIT;

Одна и та же строка, один и тот же запрос, два разных ответа в рамках одной транзакции.

Phantom read (фантомное чтение) — похоже на предыдущее, но про набор строк, а не про одну. Ты дважды выполняешь запрос с условием, и во второй раз строк становится больше или меньше, потому что кто-то вставил или удалил записи, попадающие под твой фильтр:

BEGIN;
SELECT count(*) FROM orders WHERE status = 'paid';  -- 1000
-- другая транзакция вставила новый оплаченный заказ и закоммитила
SELECT count(*) FROM orders WHERE status = 'paid';  -- 1001, "призрак" появился
COMMIT;

Три аномалии — три уровня защиты. Каждый следующий уровень изоляции убирает ещё одну.

Read Committed, Repeatable Read и Serializable — чем они отличаются?

В стандарте SQL четыре уровня: Read Uncommitted, Read Committed, Repeatable Read, Serializable. На практике в PostgreSQL живут по сути три (Read Uncommitted ведёт себя как Read Committed). Вот как они соотносятся с аномалиями:

УровеньDirty readNon-repeatable readPhantom read
Read Uncommittedвозможен (в PG — нет)возможенвозможен
Read Committedнетвозможенвозможен
Repeatable Readнетнетвозможен по стандарту (в PG — нет)
Serializableнетнетнет

Read Committed — уровень по умолчанию в PostgreSQL. Ты видишь только зафиксированные данные, грязных чтений не будет. Но каждый твой новый SELECT внутри транзакции видит свежую картину: между двумя запросами данные могут измениться. Для большинства аналитических выгрузок этого мало, когда важна консистентность.

Repeatable Read — транзакция работает с одним «снимком» (snapshot) данных, сделанным в момент её первого запроса. Что бы ни коммитили другие, ты до конца своей транзакции видишь мир таким, каким он был на старте. В PostgreSQL этот уровень заодно убирает и фантомы, так что для длинной консистентной выгрузки он часто идеален.

Serializable — самый строгий. СУБД гарантирует, что результат параллельных транзакций будет таким, будто они выполнялись строго по очереди. Защищает от всех аномалий, включая хитрые случаи вроде write skew, но платишь за это: транзакция может завершиться ошибкой сериализации, и её придётся повторить.

Задать уровень просто:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- твои запросы читают консистентный снимок
SELECT ...;
SELECT ...;
COMMIT;

Разобрать, как эти уровни всплывают на реальных задачах, удобно на практических кейсах — там как раз ситуации из работы аналитика, а не абстрактные примеры.

Как транзакции влияют на консистентность выгрузок?

Вот где теория превращается в твою боль или твоё спокойствие. Возьмём типичную задачу: выгрузить дневную выручку и число оплативших пользователей, чтобы цифры бились между собой.

Если ты считаешь выручку одним запросом, а число плательщиков — вторым, и делаешь это без транзакции на Read Committed, то между двумя запросами база успевает измениться. В первый попадёт 200 новых оплат, во второй — уже 250. Суммы не сойдутся, и это не ошибка в SQL, а гонка с продом.

Решение — обернуть обе выгрузки в одну транзакцию с консистентным снимком:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT sum(amount) AS revenue
FROM payments
WHERE paid_at >= '2026-07-11' AND status = 'succeeded';

SELECT count(DISTINCT user_id) AS payers
FROM payments
WHERE paid_at >= '2026-07-11' AND status = 'succeeded';

COMMIT;

Оба запроса видят один и тот же срез данных на момент BEGIN. Сколько бы оплат ни прилетело во время выгрузки — они синхронизированы между собой. Именно так строят согласованные срезы для метрик вроде выручки или DAU: все счётчики берутся из одного снимка.

Тот же принцип работает, когда ты тянешь данные в pandas несколькими запросами и потом джойнишь их в датафреймах. Если запросы шли вразнобой по времени, merge может дать дыры — заказ есть в одной выгрузке и отсутствует в другой. Отработать связку «SQL для среза + Python для анализа» можно в Python-тренажёре, где pandas и numpy запускаются прямо в браузере.

Блокировки и дедлоки — как не положить прод своим запросом?

Изоляция достигается блокировками и версионированием. Обычный аналитический SELECT в PostgreSQL берёт лёгкие блокировки и почти никому не мешает — MVCC (многоверсионность) позволяет читать и писать параллельно. Проблемы начинаются, когда в транзакции появляются изменения или явные блокировки строк.

Две вещи, которые стоит держать в голове.

Первое — не держи транзакцию открытой дольше необходимого. Если ты сделал BEGIN, запустил тяжёлый расчёт и ушёл на обед, не закоммитив, ты можешь блокировать очистку старых версий строк (в PG это раздувает таблицу и тормозит VACUUM) или держать блокировки, которых ждут другие.

Второе — дедлок (взаимная блокировка). Транзакция №1 захватила строку A и ждёт B, транзакция №2 захватила B и ждёт A. Обе стоят вечно. PostgreSQL это ловит и одну из транзакций убивает с ошибкой deadlock. Классический способ избегать — всегда обновлять строки в одном и том же порядке (например, по возрастанию id).

Практический вывод для аналитика: для чтения предпочитай SELECT без явных блокировок, а если нужна консистентная выгрузка — используй REPEATABLE READ вместо того, чтобы держать всё в одной длинной пишущей транзакции. Читающий снимок изолирует тебя от чужих изменений, но не встаёт у них на пути.

Что из этого спрашивают на собеседованиях и что запомнить?

Транзакции и ACID — почти обязательный блок на собеседовании аналитика и дата-инженера. Чаще всего просят: расшифровать ACID, объяснить разницу между DELETE без транзакции и с ней, назвать три аномалии чтения и сопоставить их с уровнями изоляции. Иногда дают сценарий с переводом денег и просят найти, где всё сломается. Готовые формулировки и разборы таких вопросов есть в разделе с вопросами для собеседований, а закрепить руками — на практических задачах по SQL.

Что унести с собой:

Если хочется не просто прочитать, а прогнать всё это на реальном движке — уровни изоляции, откаты, консистентные снимки, — в Pro-подписке открыты все 425 SQL-задач с настоящим PostgreSQL в браузере, разборы кейсов и AI-собеседования без лимита. Начать можно бесплатно на первых задачах в SQL-тренажёре и решить, стоит ли идти дальше.

Отработай SQL на практике
545 SQL-задач с автопроверкой в браузере — первые открыты без регистрации.
SQL-тренажёр →