MVCC: версии строк и видимость

Рейтинг: 71.7% · 16 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

MVCC: версии строк и видимость

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость (вы здесь)
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 33. MVCC: версии строк и видимость

В этом уроке разбираем механизм, на котором держится весь конкурентный доступ в postgresql - многоверсионность, или MVCC (Multi-Version Concurrency Control). Главная задача, которую он решает: дать многим транзакциям одновременно читать и писать одни и те же данные так, чтобы читающий никого не ждал, а пишущий никого не блокировал на чтении. Мы посмотрим, что такое версии строк, откуда берутся служебные поля xmin и xmax, как снимок данных (snapshot) определяет, какую версию строка покажет именно вашей транзакции, что такое HOT-обновления и почему обычные DELETE и UPDATE оставляют после себя мёртвые версии, которые потом приходится убирать через vacuum.

Изображение

Как это работает

Ключевая идея простая: строку в таблице нельзя менять на месте. Вместо этого каждое изменение порождает новую версию строки (по-русски часто говорят "кортеж", в исходниках tuple), а старая версия физически остаётся лежать в той же таблице. То есть в один момент времени на диске может существовать несколько версий одной и той же логической строки, и задача системы - показать каждой транзакции ровно ту версию, которую она имеет право видеть.

Чтобы это работало, у каждой версии есть два служебных поля. xmin - номер транзакции, которая эту версию создала (вставила или родила при обновлении). xmax - номер транзакции, которая эту версию пометила удалённой или устаревшей; пока версия живая, xmax равен нулю. Номера транзакций (их называют xid) выдаются по возрастанию, поэтому по xmin и xmax всегда можно понять относительный порядок событий.

Видимость строится на снимке (snapshot). Когда транзакция начинает читать, postgresql фиксирует снимок: какие транзакции на этот момент уже завершились успешно (зафиксированы), какие ещё в работе. Дальше для каждой версии действует грубо такое правило: версия видна, если её xmin принадлежит уже зафиксированной транзакции, а xmax либо ноль, либо принадлежит транзакции, которая ещё не зафиксирована. Иначе говоря, вы видите строку, если её "создатель" уже закоммитился, а "удалятель" - ещё нет или его нет вовсе.

Отсюда и главное свойство: читающая транзакция не блокирует пишущую и наоборот. Писатель создаёт новую версию и трогает только конкурентных писателей той же строки; читатели спокойно смотрят на старую версию по своему снимку. Именно так достигается уровень изоляции Read Committed по умолчанию - каждый новый оператор внутри транзакции берёт свежий снимок.

UPDATE технически - это не правка на месте, а удаление старой версии (проставляется xmax) плюс вставка новой. DELETE - это просто проставление xmax без новой версии. Поэтому и то, и другое плодит мёртвые версии: данные логически исчезли или поменялись, но физически старый кортеж лежит в файле, пока его не соберёт vacuum.

HOT-обновление (Heap-Only Tuple) - оптимизация для частого случая. Если UPDATE не меняет ни одного индексированного столбца и новая версия влезает на ту же страницу, postgresql связывает старую и новую версии цепочкой прямо внутри страницы и не трогает индексы. Это резко снижает раздувание индексов и нагрузку. Но стоит изменить индексированный столбец или не остаться на странице - и HOT не сработает, придётся писать во все индексы.

SQL и примеры

Посмотрим на служебные поля прямо в демобазе "Авиаперевозки" (схема bookings). Псевдостолбцы ctid, xmin, xmax есть у любой таблицы.

Код: Выделить всё

SELECT ctid, xmin, xmax, book_ref, total_amount
FROM bookings
LIMIT 5;
ctid - физический адрес версии (номер страницы, номер строки в ней), xmin - кто создал версию, xmax - ноль у живых строк. Заведём свою таблицу, чтобы безопасно ломать данные.

Код: Выделить всё

CREATE TABLE demo_mvcc (id int PRIMARY KEY, note text);
INSERT INTO demo_mvcc VALUES (1, 'first');

SELECT ctid, xmin, xmax, * FROM demo_mvcc;
Теперь обновим строку и увидим, что ctid поменялся - значит появилась новая физическая версия, а xmin вырос.

Код: Выделить всё

UPDATE demo_mvcc SET note = 'second' WHERE id = 1;
SELECT ctid, xmin, xmax, * FROM demo_mvcc;
Чтобы увидеть мёртвые версии и проверить HOT, поставьте расширение pageinspect (нужны права суперпользователя).

Код: Выделить всё

CREATE EXTENSION IF NOT EXISTS pageinspect;

SELECT lp, t_xmin, t_xmax, t_ctid
FROM heap_page_items(get_raw_page('demo_mvcc', 0));
Здесь видно сразу две версии: старая с заполненным t_xmax (помечена устаревшей) и новая. Расширение pgstattuple покажет долю мёртвых данных в таблице.

Код: Выделить всё

CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT dead_tuple_count, dead_tuple_percent
FROM pgstattuple('bookings.flights');
Текущий xid и состояние транзакции в реальном масштабе (в PG 14+ функции 64-битные, без переполнения).

Код: Выделить всё

SELECT pg_current_xact_id();             -- xid текущей транзакции (PG 13+)
SELECT pg_snapshot_xmin(pg_current_snapshot());  -- нижняя граница снимка
Частые грабли
  • Думать, что UPDATE дешевле INSERT. Под капотом UPDATE - это delete + insert; частые мелкие апдейты раздувают таблицу не меньше вставок.
  • Отключать или душить автовакуум на нагруженной таблице. Без vacuum мёртвые версии копятся, таблица и индексы пухнут (bloat), запросы замедляются.
  • Считать, что xmax больше нуля означает "строка удалена". xmax может стоять и от незавершённой транзакции, и от блокировки SELECT FOR UPDATE - строка при этом живая.
  • Полагать, что long-running транзакция безвредна. Старый снимок держит горизонт видимости и не даёт вакууму чистить мёртвые версии во всей базе.
  • Ждать HOT-обновлений при изменении индексированного столбца - их не будет, индексы перепишутся.
  • Бояться "wraparound" xid: в старых версиях это была реальная угроза, в PG 14+ счётчики 64-битные для функций, но физический формат на странице 32-битный, и aggressive vacuum по-прежнему обязателен.
Мини-лаба
  • Создайте таблицу: CREATE TABLE lab33 (id int PRIMARY KEY, v int); вставьте одну строку и запросите ctid, xmin, xmax.
  • Сделайте UPDATE значения v и снова посмотрите ctid и xmin - убедитесь, что версия сменилась.
  • Откройте второй сеанс psql. В первом начните BEGIN и сделайте UPDATE, но не коммитьте. Во втором сделайте SELECT этой строки - какое значение вы видите и почему.
  • Закоммитьте первый сеанс, повторите SELECT во втором - проследите, как сменилась видимая версия.
  • Поставьте pageinspect и через heap_page_items посмотрите все версии на нулевой странице, найдите мёртвую.
  • Выполните VACUUM lab33; и снова загляните в heap_page_items - мёртвая версия должна исчезнуть.
  • Сделайте UPDATE неиндексированного столбца и проверьте через pg_stat_user_tables (поле n_tup_hot_upd), что обновление прошло как HOT.
Контрольные вопросы
  • Что хранят поля xmin и xmax и как по ним определяется видимость версии строки?
  • Почему в MVCC читающая транзакция не блокирует пишущую?
  • Чем DELETE и UPDATE отличаются на уровне версий строк и откуда берутся мёртвые кортежи?
  • При каких условиях срабатывает HOT-обновление и что оно экономит?
  • Как long-running транзакция мешает работе vacuum и к чему это приводит?
  • Что такое snapshot и почему при Read Committed разные операторы одной транзакции могут видеть разные данные?
👍2 ❤️ 🔥1 😄 🤔1
Аватара пользователя
wingzero
Сообщения: 1
Зарегистрирован: 12 май 2026, 01:34

Re: MVCC: версии строк и видимость

Сообщение wingzero »

А правда что если держать psql открытым с незакоммиченным BEGIN полдня то вакуум по всей базе встанет колом? Поймал такое на 1с, autovacuum крутится а мёртвые версии не уходят, оказалось аналитик забыл транзакцию открытой
👍 ❤️ 🔥2 😄 🤔
Аватара пользователя
Mtoxopeus4
Сообщения: 1
Зарегистрирован: 31 май 2026, 18:14

Re: MVCC: версии строк и видимость

Сообщение Mtoxopeus4 »

Проверил n_tup_hot_upd на своей таблице - почти нули. Оказалось апдейчу столбец который висит в индексе, убрал индекс с него и HOT попёр, bloat перестал расти так быстро
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Архитектура PostgreSQL: процессы и память
Следующая глава →
VACUUM, autovacuum, freeze и wraparound

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: уровни изоляции транзакций и блокировки в postgresql

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя