how/MVCC

MVCC: одна строка, две версии

UPDATE в PostgreSQL никогда не перезаписывает строку. Он пишет новую версию, а старую штампует транзакцией, которая её устарила. Какое-то время одна строка существует на странице дважды, и две сессии могут читать две разные правды. Вот как, и как потом убирает VACUUM.

Первое, что удивляет в PostgreSQL: UPDATE не меняет строку на месте. Он пишет новую версию строки, а старую оставляет на странице, пометив устаревшей. То же и с DELETE (новой версии нет, только пометка), и с каждой строкой, которую создаёт INSERT.

Это MVCC, многоверсионное управление конкурентным доступом. У каждой версии в заголовке (см. tuple-header) два транзакционных штампа:

  • xmin - транзакция, которая эту версию создала;
  • xmax - транзакция, которая её устарила (обновила, удалила или заблокировала). xmax = 0 значит, что версия ещё жива.

Что транзакции можно увидеть, решает её снимок (см. snapshot): замороженный взгляд на то, какие транзакции были зафиксированы на момент старта. Одни и те же байты на диске разные снимки видят по-разному - именно поэтому читатели не блокируют писателей. Нажми play и проследи одну строку от единственной версии до вычищенной страницы.

step 1/5·00 · одна строка, одна версия
таблица mv · страница 0v1 · ctid (0,1)xmin 786xmax 0note 'a'жива · xmax 0читающая сессиявидит единственную версиюSELECT note FROM mv;'a'читаетодна строка, одна версия: xmin 786 её создал, xmax 0 значит жива

§ шаги

  1. Начнём с единственной строки в таблице mv, вставленной транзакцией 786:

    sql
    SELECT xmin, xmax, ctid, * FROM mv;
    --  xmin | xmax | ctid  | id | note
    -- ------+------+-------+----+------
    --   786 |    0 | (0,1) |  1 | a

    Одна версия, по адресу ctid (0,1). Её xmin равен 786 (транзакция, которая её создала), а xmax равен 0 - значит никто её не устарил: строка жива. Это и видит каждый читатель.

итого

Что важно запомнить:

  • UPDATE под капотом - это delete плюс insert: новая версия со свежим xmin и старая версия со штампом xmax = <txid обновляющей>. Обе живут на странице до уборки. Зачем так - в why-mvcc.
  • Видимость вычисляется, а не хранится. Версия видна, если её xmin зафиксирован и попадает в твой снимок, а xmax пуст или ещё не виден тебе. Полные правила - в xmin-xmax.
  • Два снимка могут законно читать два разных значения одной строки в один и тот же момент по часам. Ничего не сломано - они смотрят сквозь разные срезы времени.
  • Версия, которую больше не видит ни один живой снимок, - это мёртвый кортеж. Он всё ещё занимает место на странице, пока VACUUM не уберёт его и не освободит указатель (см. vacuum).
  • VACUUM может убрать только версии старше самого старого работающего снимка, горизонта транзакций (см. transaction-horizon). Одна долгая транзакция держит горизонт и копит мёртвые кортежи по всей базе - отсюда и берётся раздувание таблиц.
  • Когда обновление влезает на ту же страницу и ни один индекс не покрывает изменённую колонку, Postgres делает HOT-обновление (см. hot-updates) и сцепляет версии, не трогая индексы.

Выигрыш: читатели не блокируют писателей, писатели не блокируют читателей. Цена - мёртвые кортежи, и собрать их - вся работа VACUUM.

§ копнуть в базу знаний

§ попробовать руками