MVCC в PostgreSQL: как устроено и чем платим
PostgreSQL хранит несколько версий одной строки, чтобы читатели не ждали завершения записи. За это приходится платить местом на диске, работой VACUUM и вниманием к долгим транзакциям. Разбираем механизм и признаки проблем на PostgreSQL 16.

В PostgreSQL 16 запрос на чтение обычно не ждёт, пока параллельная транзакция обновит строку: он видит подходящую ему версию данных. Это работает благодаря MVCC, но у каждой записи появляется цена — старые версии нужно хранить, пока ими могут пользоваться транзакции, а затем убирать. Если очистка отстаёт, растут таблицы и индексы, а при долгих транзакциях возрастает риск проблем с заморозкой старых идентификаторов транзакций.
Как PostgreSQL выбирает версию строки
MVCC — многоверсионное управление конкурентным доступом. PostgreSQL не перезаписывает строку на месте при обычном UPDATE: он создаёт новую версию, а прежнюю помечает как больше не актуальную для транзакций, начавшихся позже. DELETE тоже не стирает данные немедленно, а отмечает версию как удалённую.
У версии строки есть служебные поля xmin и xmax. xmin указывает на транзакцию, которая создала эту версию; xmax связан с транзакцией, которая её удалила или заменила. Это не простая проверка двух чисел: PostgreSQL учитывает статус транзакций и снимок видимости — набор сведений о том, какие транзакции уже завершились, ещё идут или начались позже снимка.
Поэтому два запроса могут одновременно читать разные версии одной строки. В режиме Read Committed каждый оператор получает свой снимок. В Repeatable Read транзакция использует единый снимок, из-за чего обновлённая после его создания строка может оставаться невидимой для неё. Это часть логики изоляции, а не задержка доставки данных.
Сноска. Снимок транзакции — не копия всей базы в памяти, а набор границ и сведений, по которым PostgreSQL определяет, какие изменения доступны конкретному запросу.
Что происходит при записи
| Операция | Что происходит с версией строки | Что остаётся для очистки |
|---|---|---|
| INSERT | Создаётся новая версия | После отмены транзакции версия становится невидимой |
| UPDATE | Создаётся новая версия, прежняя помечается как заменённая | Старая версия и, иногда, старые записи индексов |
| DELETE | Текущая версия помечается как удалённая | Удалённая версия и связанные с ней записи индексов |
Если обновляются только поля, не входящие в индексы, PostgreSQL иногда может выполнить HOT update. Для этого на странице таблицы должно хватить места для новой версии. Такой вариант помогает избежать создания новых записей индексов, но не отменяет появление версии строки и её последующую очистку. Если меняется индексируемое поле или свободного места на странице недостаточно, HOT update не подходит.
Счёт за MVCC: место, очистка и задержки
Старые версии нельзя удалять сразу: открытая транзакция может всё ещё читать снимок, в котором эти версии нужны. Когда ни один снимок больше не может их видеть, VACUUM получает возможность освободить место для повторного использования. Обычный VACUUM, как правило, не уменьшает файл таблицы на диске: он делает место доступным для будущих записей. Для физического переписывания таблицы есть VACUUM FULL, но он требует более серьёзной блокировки и дополнительного места на время работы.
Цена зависит от характера нагрузки. Частые UPDATE и DELETE создают больше устаревших версий; индексы добавляют работу по очистке; долгие транзакции могут задержать её. Если очистка не поспевает, таблица разрастается, растут затраты на чтение и обслуживание, а автovacuum вынужден работать активнее. Это не означает, что каждый UPDATE обязательно создаёт новую запись в каждом индексе: HOT update как раз может этого избежать.
MVCC переносит ожидание с читателя на хранение и уборку. Пока старый снимок нужен транзакции, база не может просто выбросить старую версию.
За уборку обычно отвечает autovacuum — фоновые процессы, которые запускают обслуживание таблиц по настроенным условиям. Универсального порога, после которого очистка гарантированно успеет, нет: важны частота изменений, размер таблиц, конфигурация и длительность транзакций. Поэтому сам факт включённого autovacuum не доказывает, что конкретная таблица обслуживается достаточно быстро.
Где подход ломается
Самый распространённый тормоз — транзакция, которая открыта долго, даже если приложение почти ничего не делает. Например, приложение начало транзакцию, ждёт ответа пользователя или внешнего сервиса и только потом завершает её. Пока сохраняется старый снимок, VACUUM может быть не в состоянии убрать некоторые версии.
Похожий эффект могут дать слоты репликации: они способны удерживать старые версии для потребителя, который отстаёт. Не удаляйте слот вслепую — сначала выясните, используется ли он и почему отстаёт репликация. Ещё один риск — возраст идентификаторов транзакций. PostgreSQL должен периодически замораживать старые версии, чтобы корректно работать при циклическом использовании идентификаторов; задержка обслуживания может привести к защитным ограничениям на запись.
- Таблица растёт, а изменения данных не стали чаще. Сравните число оценочных мёртвых строк и частоту autovacuum; проверьте, нет ли долгих транзакций и удерживающих очистку слотов.
- VACUUM проходит, но размер файла не падает. Это ожидаемо для обычного VACUUM: освободившееся место обычно остаётся внутри файла для повторного использования.
- Задержка появилась на активно обновляемой таблице. Проверьте, меняются ли индексируемые поля и возможен ли HOT update; отдельно посмотрите на нагрузку записи и обслуживание индексов.
- Растёт возраст XID. Это сигнал проверить состояние autovacuum, долгие транзакции и репликационные слоты, а не повод запускать принудительную очистку без диагностики.
Что измерять на PostgreSQL 16
Для первичной проверки подойдут системные представления. Значение n_dead_tup — оценка, а не точный подсчёт строк, поэтому смотрите на динамику и сопоставляйте её с размером таблицы, объёмом записи и временем последней очистки.
SELECT schemaname, relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
Найдите транзакции, которые открыты дольше остальных. Само по себе большое время не доказывает проблему, но долгую транзакцию стоит проверить вместе с ростом мёртвых версий и задержкой autovacuum.
SELECT pid, now() - xact_start AS xact_age, state,
wait_event_type, wait_event
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
Отдельно следите за возрастом идентификаторов транзакций в базах. Это не таймер до аварии, а показатель для наблюдения в динамике: при росте выясняйте причину и сверяйтесь с рекомендациями документации PostgreSQL 16.
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
Что делать дальше
Сначала соберите исходную картину: снимите значения системных представлений в обычный и пиковый периоды, отметьте время autovacuum и найдите транзакции с большим возрастом. Если показатель растёт, проверьте код приложения на транзакции, которые переживают сетевые ожидания, и выясните, не удерживает ли очистку слот репликации. Затем наблюдайте, меняется ли число мёртвых строк после устранения причины. Настройки autovacuum и пороги меняйте по результатам измерений на конкретных таблицах, а не по одному универсальному рецепту.
Эта статья разбирает механику MVCC и стоимость хранения версий в PostgreSQL 16. Настройка репликации, выбор уровня изоляции и устранение разрастания конкретной таблицы требуют отдельной диагностики.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: PostgreSQL 16: Introduction to MVCC, PostgreSQL 16: Routine Vacuuming
Частые вопросы
Почему обычный VACUUM не уменьшил размер таблицы на диске?
Может ли одна долгая транзакция остановить очистку всей базы?
Нормально ли, что n_dead_tup показывает много мёртвых строк?
Как понять, что пора бояться переполнения идентификаторов транзакций?
Что делать, если в продакшене внезапно вырос размер таблицы?
Будут ли эти объяснения и запросы работать в другой версии PostgreSQL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.