MVCC в PostgreSQL: как версии строк управляют чтением
В PostgreSQL команда UPDATE обычно не переписывает строку на месте: она создаёт новую версию, а старую оставляет для транзакций с прежним снимком. Разбираем, как сервер выбирает видимую версию и почему долгие транзакции мешают очистке таблиц.

В PostgreSQL 16 после UPDATE старую версию строки могут продолжать видеть уже начавшиеся транзакции, а новые запросы — читать обновлённую. Это не исключение и не кеш: так работает MVCC, многоверсионный контроль параллельного доступа. Он помогает читателям и писателям не блокировать друг друга на каждой строке, но оставляет после изменений версии, которые нужно вовремя очищать.
Что именно хранит PostgreSQL
В обычной таблице PostgreSQL строка хранится как версия записи в куче (heap). У версии есть служебные поля, в том числе xmin и xmax. В упрощённом виде xmin указывает транзакцию, создавшую версию, а xmax — транзакцию, которая удалила её или заменила при обновлении. Реальная проверка видимости учитывает также состояние транзакций и снимок, поэтому читать эти поля как обычные «дата создания» и «дата удаления» нельзя.
Когда транзакция выполняет UPDATE, PostgreSQL обычно создаёт новую версию строки. Старая не исчезает сразу: сервер помечает её как устаревшую для транзакций, которые увидят изменение, и создаёт новую с собственным xmin. DELETE тоже не обязан физически стирать строку немедленно — он делает её невидимой для подходящих будущих снимков.
У этого решения есть цена. Частые UPDATE и DELETE создают мёртвые версии, а таблицы и индексы могут занимать больше места, чем текущие данные. MVCC переносит часть затрат с момента изменения на последующую уборку и требует следить за тем, сколько старых версий ещё нужно транзакциям.
MVCC не хранит «последнее состояние строки». Он хранит несколько версий, а затем решает, какую из них разрешено видеть конкретной транзакции.
Как выбирается видимая версия
Снимок транзакции — граница видимости
Снимок (snapshot) описывает состояние транзакций в момент, когда запросу нужно зафиксировать, какие изменения считать уже произошедшими. При проверке версии PostgreSQL выясняет, завершилась ли транзакция, создавшая её, и попадает ли это создание в снимок читателя. Если другая транзакция ещё не завершилась на момент снимка, её изменение не становится видимым только потому, что та успела записать новую версию.
В режиме READ COMMITTED каждый SQL-оператор получает актуальный снимок. Поэтому два SELECT в одной транзакции могут увидеть разные зафиксированные изменения. В REPEATABLE READ снимок сохраняется для транзакции после первого обычного оператора, и повторное чтение не подхватывает более поздние фиксации. SERIALIZABLE использует снимок и дополнительно отслеживает зависимости, чтобы обнаруживать некоторые последовательности, которые нельзя безопасно выполнить как сериализованные.
Отсюда следует важное различие: «запрос не видит изменение» не всегда означает, что изменения нет. Оно могло быть зафиксировано после того, как запрос получил снимок. И наоборот, новый запрос в READ COMMITTED может увидеть его, даже если он выполняется в той же транзакции.
| Ситуация | Что видит читатель | Практическое следствие |
|---|---|---|
| UPDATE ещё не зафиксирован | Чужая транзакция не видит её новую версию | Читатель получает согласованное состояние, а не промежуточную запись |
| UPDATE зафиксирован после снимка READ COMMITTED | Текущий оператор остаётся на прежнем снимке | Следующий оператор может увидеть обновлённую версию |
| UPDATE зафиксирован после снимка REPEATABLE READ | Транзакция продолжает читать по своему снимку | Повторное чтение может вернуть прежнее значение |
| Старая версия больше не нужна активным снимкам | Обычный SELECT её уже не выбирает | VACUUM может очистить место для повторного использования |
Почему читатель обычно не блокирует писателя
Обычный SELECT не ставит блокировку на каждую прочитанную строку, которая запрещает другому запросу выполнить UPDATE. Читатель выбирает подходящую версию по своему снимку, а писатель создаёт новую. Это снижает взаимные блокировки при типичной нагрузке на чтение и запись.
Но MVCC не отменяет блокировки вообще. UPDATE может ждать другой UPDATE той же строки; явные блокировки строк влияют на конкурирующие операции, а DDL и блокировки таблиц способны задержать запросы. Если операция внезапно ждёт, недостаточно проверить только текст SQL: выясните, какой тип блокировки удерживается и какой запрос её ждёт.
Термин. Снимок транзакции — зафиксированный для чтения набор сведений о состоянии транзакций. Он задаёт, какие изменения считаются уже видимыми, а какие для данного запроса ещё не наступили.
Что делает VACUUM и почему он не уменьшает файл сразу
После завершения транзакций старые версии иногда ещё нужны другим активным снимкам. Когда ни один такой снимок уже не может их увидеть, VACUUM получает возможность освободить их место для будущих записей и выполнить связанную уборку индексов. Это не то же самое, что немедленно вернуть освободившееся место операционной системе: обычный VACUUM чаще делает его доступным для повторного использования внутри таблицы.
В PostgreSQL работает autovacuum: фоновые процессы запускают обслуживание по настраиваемым условиям и следят, в частности, за накоплением мёртвых версий и статистикой таблиц. Он не гарантирует, что любая таблица будет очищена именно в удобный для приложения момент. Высокая скорость изменений, неподходящие пороги, нехватка ресурсов и слишком старый горизонт видимости могут привести к тому, что версии копятся быстрее, чем исчезают.
Проблема часто не в «медленном VACUUM», а в транзакции, которая удерживает старый снимок. Пока он потенциально может увидеть прежнюю версию, очистка не вправе её убрать. Такие транзакции бывают не только активными запросами: приложение может открыть транзакцию и оставить соединение бездействовать. Ограничивать уборку также способны удерживаемые горизонты репликационных слотов.
- В
pg_stat_activityсмотрите на время начала транзакции (xact_start) и удерживаемый снимок (backend_xmin), если поле заполнено. - В
pg_stat_user_tablesотслеживайте оценку числа мёртвых строкn_dead_tup, а также время последнего autovacuum. Это ориентиры, а не точный подсчёт всех неубранных версий. - В
pg_replication_slotsпроверяйте, удерживают ли слоты значенияxminилиcatalog_xmin. - Для риска переполнения счётчика транзакций следите за возрастом
datfrozenxidи настройками обслуживания. Заморозка старых идентификаторов нужна, чтобы PostgreSQL мог безопасно различать транзакции при циклическом использовании их номеров.
VACUUM FULL решает другую задачу: переписывает таблицу, чтобы плотнее разместить данные и вернуть файловое пространство системе. Это более тяжёлая операция с блокировкой, поэтому считать её обычной заменой autovacuum нельзя. Перед ней нужно оценить размер таблицы, окно обслуживания и влияние на доступность.
Где MVCC создаёт проблемы
Долгие транзакции и разрастание таблиц
Пока старый снимок нужен транзакции, устаревшие версии остаются недоступны для окончательной очистки. Если приложение держит транзакции открытыми во время ожидания пользователя, внешнего API или долгой фоновой задачи, таблица может расти, а autovacuum — не успевать освобождать место. Особенно неприятно, когда соединение числится «idle in transaction»: запросов нет, но транзакция ещё не закрыта.
Сначала найдите владельца самой старой транзакции и выясните, почему она открыта. Затем проверьте расписание autovacuum, скорость записи и удержания слотов. Не повышайте пороги уборки вслепую: это может увеличить нагрузку на диск и CPU, но не устранит старый снимок, который запрещает очистку.
Индексы и HOT-обновления
При UPDATE PostgreSQL обычно должен обеспечить доступ к новой версии и через индексы. Если обновляемые значения не затрагивают индексируемые столбцы и на странице таблицы есть место, может сработать HOT-обновление (Heap-Only Tuple): новая версия остаётся в той же странице, а создавать новые записи индексов не требуется. Это сокращает часть накладных расходов, но не превращает UPDATE в бесплатную операцию: версия строки всё равно появляется, а её последующая очистка всё ещё нужна.
Если обновлять индексируемый столбец или на странице не хватает места, условия для HOT могут не выполниться. Поэтому при массовых обновлениях важны не только число запросов и размер таблицы, но и состав индексов, доля изменяемых столбцов и свободное место на страницах.
Что мерить на работающей системе
Без данных о конкретной нагрузке нельзя честно назвать универсальную частоту autovacuum или «нормальный» процент мёртвых строк. Замеряйте динамику: как растут таблицы и индексы, меняется ли n_dead_tup, сколько длится самый старый снимок, есть ли удерживающие горизонт репликационные слоты и как часто запускается обслуживание. Сопоставляйте эти наблюдения с объёмом изменений и временем выполнения запросов.
Если проблема проявляется как рост дискового использования, отдельно выясните, что именно растёт: таблица, индексы или журналы WAL. Если растут задержки, проверьте ожидания блокировок и нагрузку диска, а не списывайте их на MVCC по одному признаку. Сравнивайте показатели до и после изменения параметров на одной и той же нагрузке; иначе эффект настройки легко перепутать с изменением трафика.
Что делать дальше
Начните с самых долгих открытых транзакций и таблиц, где быстрее всего растут мёртвые версии. Затем проверьте удержания replication slots и фактическое расписание autovacuum. Для PostgreSQL 16 сверяйте детали видимости и обслуживания с документацией именно этой версии: пороги autovacuum и подход к настройке зависят от характера записи, размера таблиц и доступных ресурсов.
Инвалидация кеша и устройство репликации — отдельные темы; здесь речь только о видимости версий и их очистке.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: PostgreSQL 16: Introduction to MVCC, PostgreSQL 16: Routine Vacuuming
Частые вопросы
Почему после UPDATE таблица занимает больше места, хотя строк столько же?
Может ли обычный SELECT заблокировать UPDATE?
Почему autovacuum не удаляет старые версии?
Нужно ли запускать VACUUM FULL, если таблица разрослась?
Чем READ COMMITTED отличается от REPEATABLE READ в PostgreSQL 16?
Как понять, что разрастание связано именно с MVCC?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.