Модули 4 (MVCC) и 10 (блокировки) дали нам инструменты — версии строк, snapshot, row-level locks. Этот модуль про то, какие гарантии из этих инструментов складываются и как разработчику об этом думать. Начнём с базы: что говорит SQL-стандарт, какие аномалии он описывает и почему Postgres почти везде ведёт себя строже, чем обязан.
Четыре уровня по ANSI SQL-92
Стандарт SQL-92 определяет четыре уровня изоляции через разрешённые аномалии. Не через «как реализовано», а через «что может пойти не так»:
Чем выше уровень, тем меньше аномалий разрешено. SERIALIZABLE — единственный уровень, обещающий результат, эквивалентный какому-то последовательному выполнению транзакций.
Разберём аномалии:
- Dirty read — T1 прочитала строку, которую T2 только что записала, но ещё не закоммитила. Если T2 потом сделает
ROLLBACK, то T1 видела значение, которого в БД никогда не было. - Non-repeatable read — T1 прочитала одну и ту же строку дважды, и между чтениями T2 успела закоммитить
UPDATE. Два значения в одной транзакции — разные. - Phantom read — T1 выполнила
SELECT ... WHERE x > 10дважды, и между этими SELECT’ами T2 вставила новую строку, удовлетворяющую условию. Множество строк изменилось. - Lost update (формально не входит в SQL-92, но описан в Berenson et al. 1995) — T1 и T2 одновременно делают
read-modify-writeодной строки, и одно из обновлений «теряется».
Что Postgres реально предоставляет
Postgres даёт четыре уровня по имени, но фактически только три различных:
READ UNCOMMITTED в Postgres = READ COMMITTED (dirty read невозможен в MVCC). REPEATABLE READ = snapshot isolation. SERIALIZABLE с 9.1 — это SSI (Serializable Snapshot Isolation).
Ключевые отличия от стандарта:
- READ UNCOMMITTED в Postgres невозможен физически. MVCC основан на snapshot’ах, видимость определяется через
xmin/xmax. Грязное чтение требует увидеть кортеж сxminот незакоммиченной транзакции — Postgres такие кортежи отфильтровывает на этапе visibility check. ПоэтомуSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTEDмолча работает как READ COMMITTED. - REPEATABLE READ в Postgres сильнее стандарта: phantom read невозможен (snapshot фиксируется на первом statement и держится до конца транзакции). То, что ANSI называет REPEATABLE READ — это в Postgres snapshot isolation, который запрещает phantom, но допускает write skew.
- SERIALIZABLE — это : snapshot isolation + детектор циклов в графе зависимостей. О том, как это работает — уроки 3 и 4.SSI
Текущий уровень транзакции
Уровень задаётся на уровне транзакции, а не сессии (хотя default_transaction_isolation устанавливает дефолт сессии):
Просмотр текущего уровня изоляции и default'а сессии. Перед BEGIN или внутри транзакции до первого запроса можно сменить уровень через SET TRANSACTION.
Сменить уровень для одной транзакции:
Переключение на REPEATABLE READ для конкретной транзакции. После COMMIT уровень вернётся к default.
Важно: SET TRANSACTION ISOLATION LEVEL действует, только если выполнен до первой команды транзакции. Иначе — ошибка SET TRANSACTION ISOLATION LEVEL must be called before any query.
Дорожки T1 / T2: как читать concurrency-диаграммы
В этом модуле мы будем часто рисовать две параллельные транзакции. Условимся о нотации:
Время идёт сверху вниз. Каждая строка — один statement. Колонка T1 слева, T2 справа. Пустая ячейка значит, что в этот момент эта транзакция ничего не делает.
В pglite-песочнице, через которую мы работаем в этом курсе, одна connection — параллельных транзакций технически нет. Поэтому аномалии разбираем текстом + диаграммой, а в sandbox запускаем шаг-в-шаг изменения одной транзакции, чтобы проверить семантику изоляции.
Маленький эксперимент: non-repeatable read под READ COMMITTED
Покажем правую часть: что READ COMMITTED разрешает non-repeatable read. Здесь мы эмулируем «T2 закоммитила между двумя SELECT T1» через явный коммит подзапроса:
READ COMMITTED: каждый statement получает свой snapshot. Если бы между двумя SELECT в реальном мире другая транзакция сделала COMMIT — второй SELECT увидел бы новое значение. Здесь эмулируем серией коммитов.
А теперь то же самое, но в REPEATABLE READ:
REPEATABLE READ: snapshot фиксируется на первом statement. Все последующие чтения внутри транзакции видят то же состояние, даже если другие транзакции коммитят изменения.
В реальном мире второй SELECT мы бы запустили после внешнего UPDATE, но для понимания семантики достаточно знать: snapshot держится на всю транзакцию.
Чего нет в Postgres
Несколько артефактов SQL-стандарта в Postgres не реализованы или работают «иначе»:
- DEFERRABLE constraints внутри REPEATABLE READ — работают, но не пересекаются с моделью snapshot. Постановка
SET CONSTRAINTS ALL DEFERREDоткладывает проверку до COMMIT, и если в момент COMMIT’а констрейнт нарушен — транзакция падает. - Cursor stability (уровень из DB2) — отсутствует. Любой курсор внутри REPEATABLE READ-транзакции работает по snapshot, как обычный SELECT.
- READ UNCOMMITTED — как уже сказали, физически невозможен. Postgres документирует это явно.
Что выбирать по умолчанию
В 95% production-приложений на Postgres — READ COMMITTED (default). Причины:
- Минимальный overhead: snapshot пересчитывается на каждом statement, мёртвые tuple’ы виднее VACUUM’у быстрее.
- Меньше serialization failures: ни SI, ни SSI не abort’ят транзакцию при конкуренции — просто видят более старое состояние или ждут lock.
- Большинство ORM (Hibernate, Sequelize, ActiveRecord) предполагает RC: их retry-логика заточена под него.
REPEATABLE READ берут, когда внутри транзакции много чтений одной и той же таблицы и нужна стабильная картина мира. Аналитика, отчёты, перенос данных батчем.
SERIALIZABLE берут, когда в коде есть инварианты «прочитал-проверил-записал», которые легко сломать через write skew (модуль 4 урок 4). Цена — нужно научить приложение retry’ить транзакции с ошибкой 40001 serialization_failure.
В следующем уроке погружаемся в snapshot isolation — как именно Postgres решает, что видно, а что нет, и как через snapshot detect’ится write-write conflict.
Чек-лист
- ANSI SQL-92 определяет 4 уровня через разрешённые аномалии: dirty read, non-repeatable read, phantom.
- Berenson et al. (1995) добавили lost update и write skew — последний SQL-92 не описывает вовсе.
- Postgres: READ UNCOMMITTED = READ COMMITTED (нет грязных чтений в MVCC). REPEATABLE READ = snapshot isolation. SERIALIZABLE = SSI.
- Уровень устанавливается на транзакцию через
BEGIN ISOLATION LEVEL XилиSET TRANSACTION ISOLATION LEVEL Xдо первого statement. - Default —
READ COMMITTED. Поднимать стоит точечно, с пониманием цены: bloat, serialization failures, retry-логика.