# Зачем многоверсионность _MVCC и видимость · PostgreSQL Knowledge Base_ **TL;DR:** PostgreSQL не меняет строку на месте. UPDATE пишет новую версию рядом и помечает старую как устаревшую, DELETE только помечает. Поэтому читающие транзакции не ждут пишущих, а пишущие не ждут читающих: каждый видит свою версию. Плата за это - мёртвые версии, которые потом убирает vacuum. Представь банковскую таблицу. Один запрос считает общий баланс по всем счетам - читает миллион строк. В это же время идёт перевод денег между двумя счетами. Если перевод поменяет строки прямо под носом у отчёта, отчёт получит несогласованную картину: деньги уже списаны с одного счёта, но ещё не зачислены на другой. Старое решение - блокировки: пишущий запирает строку, читающие ждут. Просто, но медленно: отчёт на миллион строк остановит все переводы. PostgreSQL идёт другим путём - **многоверсионностью** (MVCC, Multi-Version Concurrency Control). ## Главная идея: не менять, а добавлять Строка не правится на месте. Вместо этого: | Операция | Что физически происходит | |---|---| | INSERT | появляется новая версия строки | | UPDATE | старая версия помечается устаревшей, рядом пишется новая | | DELETE | версия помечается устаревшей, ничего не пишется | Каждая версия помнит, какая транзакция её создала (`xmin`) и какая пометила устаревшей (`xmax`). По этим числам любая транзакция решает, видеть ей эту версию или нет. Правила разбирает [xmin-xmax](/courses/postgres/kb/xmin-xmax.md), а сами поля лежат в [tuple-header](/courses/postgres/kb/tuple-header.md). ## UPDATE - это не изменение, а DELETE плюс INSERT Проще всего увидеть это на системных колонках: ```sql CREATE TABLE mv (id int, note text); INSERT INTO mv VALUES (1, 'a'); SELECT xmin, xmax, ctid FROM mv; -- xmin=786, xmax=0, ctid=(0,1) UPDATE mv SET note = 'b' WHERE id = 1; SELECT xmin, xmax, ctid FROM mv; -- новая версия: другой xmin, ctid=(0,2) ``` `SELECT` всегда показывает ту версию, что видна твоей транзакции. После UPDATE это новая версия с новым `ctid`. Старая никуда не делась - она лежит рядом и видна транзакциям, которые начались до коммита обновления. ## Читатели и писатели не мешают друг другу Из этой схемы следует главное свойство: **читающий запрос никогда не блокирует пишущий и наоборот**. Отчёт по балансу видит версии, какими они были на момент его старта (его снимок - см. [snapshot](/courses/postgres/kb/snapshot.md)). Перевод денег тем временем спокойно пишет новые версии. Никто никого не ждёт. ## Цена: мусор и vacuum Бесплатно это не даётся. Каждый UPDATE и DELETE оставляет мёртвые версии, которые занимают место, пока их видит хоть одна транзакция. Когда последняя такая транзакция завершится, версии становятся мусором. Убирает его vacuum (см. [vacuum](/courses/postgres/kb/vacuum.md)). Поэтому активно изменяемая таблица в PostgreSQL всегда чуть больше, чем «чистый» объём данных, и требует регулярной очистки. Это не баг, а обратная сторона того, что никто никого не ждёт. ## Команды ```sql SELECT xmin, xmax, ctid, * FROM mv; ``` Системные колонки версии: кто создал, кто пометил устаревшей, адрес ```sql SELECT n_live_tup, n_dead_tup FROM pg_stat_all_tables WHERE relname = 'mv'; ``` Сколько живых и мёртвых версий накопила таблица ```sql UPDATE mv SET note = note WHERE id = 1; SELECT ctid FROM mv WHERE id = 1; ``` Даже пустое по смыслу обновление создаёт новую версию с новым ctid ## См. также - [xmin, xmax и правила видимости](/courses/postgres/kb/xmin-xmax.md) - [Снимок данных (snapshot)](/courses/postgres/kb/snapshot.md) - [Заголовок кортежа](/courses/postgres/kb/tuple-header.md) - [Уровни изоляции в PostgreSQL](/courses/postgres/kb/isolation-levels.md) - [VACUUM и removable cutoff](/courses/postgres/kb/vacuum.md)