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


Страницы и кортежи