ACID в PostgreSQL
Предпосылки: ACID.
ACID описывает контракт транзакции в общем виде. PostgreSQL реализует каждую гарантию конкретными механизмами, и они переплетены: одна операция UPDATE задействует все четыре.
Версионирование вместо перезаписи
Ключевое архитектурное решение PostgreSQL — версионирование строк (MVCC) вместо обновления на месте. UPDATE не перезаписывает старый кортеж, а создаёт новую версию рядом с ним. Из этого решения вытекает и атомарность (откат — это просто пометка версии как невидимой), и изоляция (каждая транзакция видит свой набор версий через снимок). Ограничения проверяют валидность новой версии. WAL гарантирует, что COMMIT переживёт сбой. Очистка мёртвых версий — цена, которую платит VACUUM.
Один UPDATE внутри BEGIN...COMMIT проходит через все четыре механизма:
UPDATE accounts SET balance = balance - 500 WHERE id = 1
Создать новую версию строки (xmin) --> Atomicity (MVCC)
Определить видимость для других tx --> Isolation (snapshots)
Проверить ограничения таблицы --> Consistency (constraints)
Записать WAL-запись, fsync --> Durability (WAL)
Старая версия --> dead tuple --> Cost (VACUUM)
UPDATE создаёт новую версию
PostgreSQL находит строку на странице данных, но не модифицирует её. Вместо этого он создаёт новую версию кортежа: поле xmin новой версии содержит идентификатор текущей транзакции, а поле xmax старой версии указывает на ту же транзакцию. Если транзакция откатывается, журнал статусов транзакций (CLOG) помечает её как aborted — новая версия становится невидимой для всех, а старая остаётся видимой. Undo-лог не нужен: старые данные физически на месте. Цена — мёртвые кортежи (dead tuples), которые копятся до прихода VACUUM.
Новая версия создана, но параллельно работают другие транзакции. Кто видит старую версию, а кто новую?
Кто видит какую версию
Видимость определяется снимком (snapshot): каждая транзакция фиксирует, какие транзакции уже завершены. Параллельный SELECT balance FROM accounts WHERE id = 1 сверяет xmin/xmax кортежа со своим снимком и видит ту версию, которая была зафиксирована до момента снимка. Читатели не блокируют писателей, писатели не блокируют читателей.
Если два UPDATE пытаются изменить одну и ту же строку, второй ждёт на блокировке строки — единственная точка, где MVCC требует ожидания.
PostgreSQL предоставляет три уровня изоляции: READ COMMITTED (снимок на каждый оператор), REPEATABLE READ (снимок на транзакцию), SERIALIZABLE (снимок + отслеживание зависимостей через SIREAD locks).
Версионирование и видимость на месте, но что мешает записать невалидное значение?
Проверка ограничений
В момент UPDATE PostgreSQL проверяет все ограничения таблицы: NOT NULL, UNIQUE, FOREIGN KEY, CHECK, EXCLUDE. Если новое значение balance нарушает CHECK (balance >= 0), оператор (или вся транзакция) откатывается. СУБД гарантирует собственные инварианты; бизнес-правила за пределами ограничений остаются ответственностью приложения.
Механика обновления и валидации работает в памяти и на страницах данных. Но если сервер упадёт после COMMIT, выживут ли изменения?
COMMIT переживает сбой
При COMMIT PostgreSQL записывает WAL-запись, описывающую изменение, и вызывает fsync на файле журнала предзаписи. Клиент получает подтверждение только после того, как запись оказалась на диске. Страница данных с новым кортежем может оставаться грязной в буферном кеше — фоновый писатель или checkpoint запишет её позже. Если сервер падает до записи грязной страницы, recovery воспроизводит WAL и восстанавливает изменение.
Цена гарантий начинается с физического хранения — страницы и кортежи определяют, как данные лежат на диске и как PostgreSQL управляет версиями строк.
Sources
- PostgreSQL Documentation (пример: v16): Transactions и Transaction Isolation. https://www.postgresql.org/docs/16/tutorial-transactions.html, https://www.postgresql.org/docs/16/transaction-iso.html