Представь банковскую таблицу. Один запрос считает общий баланс по всем счетам - читает миллион строк. В это же время идёт перевод денег между двумя счетами. Если перевод поменяет строки прямо под носом у отчёта, отчёт получит несогласованную картину: деньги уже списаны с одного счёта, но ещё не зачислены на другой.
Старое решение - блокировки: пишущий запирает строку, читающие ждут. Просто, но медленно: отчёт на миллион строк остановит все переводы. PostgreSQL идёт другим путём - многоверсионностью (MVCC, Multi-Version Concurrency Control).
Главная идея: не менять, а добавлять
Строка не правится на месте. Вместо этого:
| Операция | Что физически происходит |
|---|---|
| INSERT | появляется новая версия строки |
| UPDATE | старая версия помечается устаревшей, рядом пишется новая |
| DELETE | версия помечается устаревшей, ничего не пишется |
Каждая версия помнит, какая транзакция её создала (xmin) и какая
пометила устаревшей (xmax). По этим числам любая транзакция решает,
видеть ей эту версию или нет. Правила разбирает xmin-xmax, а сами поля
лежат в tuple-header.
UPDATE - это не изменение, а DELETE плюс INSERT
Проще всего увидеть это на системных колонках:
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). Перевод денег тем временем спокойно пишет новые версии. Никто никого не ждёт.
Цена: мусор и vacuum
Бесплатно это не даётся. Каждый UPDATE и DELETE оставляет мёртвые версии, которые занимают место, пока их видит хоть одна транзакция. Когда последняя такая транзакция завершится, версии становятся мусором. Убирает его vacuum (см. vacuum). Поэтому активно изменяемая таблица в PostgreSQL всегда чуть больше, чем «чистый» объём данных, и требует регулярной очистки. Это не баг, а обратная сторона того, что никто никого не ждёт.