Уровень 2.0
Предусловие:
- Изучен модуль «Структура данных» данного курса
В этом задании вас ждут:
- Понятие снимка и правила видимости
- Структура снимка данных
- Построение снимка для различных уровней изоляции
- Правила видимости при использовании курсора
- Горизонт транзакции
- Горизонт очистки
Понятие снимка и правила видимости
Версионность, обеспечиваемая механизмом MVCC, позволяет изолировать параллельные транзакции, используя минимум блокировок.
Для этого каждая транзакция работает со своим набором версий строк, образующим согласованную картину данных на определенный момент времени. Такой набор называется снимком и содержит последние версии строк, зафиксированные до момента его создания. В снимок попадает только одна версия каждой строки.
Базовое правило видимости версий строк в снимке таково: версия строки видна в снимке, если изменения транзакции с номером xmin в снимке видны, а изменения транзакции с номером xmax не видны.
Здесь под номерами xmin и xmax понимаются соответствующие значения из заголовка версии строки. Однако, как будет показано ниже, это не единственная возможная интерпретация указанных номеров.
В то же время изменения транзакции в снимке видны, если либо транзакция была зафиксирована до момента создания снимка, либо транзакция сама создала снимок (свои незафиксированные изменения транзакция должна видеть).
Пример снимка данных представлен на следующем рисунке:

На рисунке строка 1 не попала в снимок, так как ее единственная версия была удалена транзакцией с номером xmax, изменения которой были видны в момент создания снимка.
Вторая версия строки 2 попала в снимок: транзакция с номером xmin указанной версии зафиксирована до момента создания снимка и никакая транзакция ее не удалила до момента создания снимка.
Также в снимок попала вторая версия строки 3. Несмотря на то что указанная версия к настоящему моменту была удалена транзакцией с номером xmax, на момент создания снимка изменения этой транзакции видны не были.
Также в снимок попала первая версия строки 4, несмотря на то что она была удалена до создания снимка транзакцией с номером xmax. В данном случае изменения транзакции с номером xmax не видны, поскольку произошел ее откат (aborted).
И наконец, единственная версия строки 5 также попала в снимок, несмотря на то что создавшая ее транзакция с номером xmin не была завершена к моменту создания снимка. Дело в том, что указанная транзакция является той, что создала снимок. Свои незафиксированные изменения она видит.
Однако рассмотренное правило видимости не является единственным. Также имеются некоторые специальные правила, учитывающие в том числе возможность заморозки номеров транзакций xmin, а также частичной видимости собственных изменений внутри транзакции при использовании курсоров. Последнее правило будет подробнее рассмотрено в данной лекции.
Структура снимка данных
Очевидно, что снимок не является физической копией данных. Иначе зачем нужен был бы механизм многоверсионности? Ниже рассматривается структура снимка данных, включающая информацию, достаточную для определения видимости версий строк в нем.
Итак, для определения видимости версии строки в снимке нам нужна следующая информация:
- Номер транзакции, создавшей версию строки
- Номер транзакции, удалившей версию строки
- Момент создания снимка
- Статусы транзакций с номерами
xminиxmaxна момент создания снимка
Как уже известно, номера транзакций, создавшей и удалившей версию строки, имеются в ее заголовке, это поля xmin и xmax соответственно.
Момент создания снимка может быть определен с использованием счетчика транзакций, поскольку номера транзакций в Pangolin выдаются последовательно и по сути являются внутренними его часами. Также и момент начала транзакции может быть определен ее номером в заголовке версии строки.
При этом момент завершения транзакции не сохраняется ни в заголовке версии строки, ни в какой-либо другой структуре, используемой механизмом MVCC. Поэтому определить статус транзакции, который был в момент создания снимка, после создания снимка уже не представляется возможным.
Единственное, что возможно определить, это статус транзакций на текущий момент. Сделать это можно с использованием структуры ProcArray, содержащей список всех активных обслуживающих процессов и номеров их транзакций, а также журнала статусов транзакций CLOG или соответствующих информационных битов в заголовке версии строки.
Исходя из этого, при построении снимка данных обслуживающий процесс запоминает следующую информацию:
xmin(не путать сxminв заголовке версии строки) — минимальный номер среди всех активных транзакций. Изменения зафиксированных транзакций с меньшими номерами безусловно видны в снимкеxmax(не путать сxmaxв заголовке версии строки) — номер на один больше номера последней зафиксированной транзакции. Транзакции с большими или равнымиxmaxномерами в момент построения снимка еще не завершены или даже не начаты, поэтому их изменения в снимке не видны. Номерxmaxопределяет момент времени, в который снимок был сделанxip_list(xid in progress list) — список номеров активных транзакций с номерами, меньшимиxmaxна момент построения снимка. Данный список необходим как раз из-за отсутствия информации о моменте завершения транзакций
Совокупность перечисленных параметров как раз и определяет снимок данных.
В снимке видны изменения зафиксированных транзакций с номерами xid, удовлетворяющими одному из следующих условий:
xid < xminxmin <= xid<xmaxиxidне входит в списокxip_list
Указанные условия определяют список транзакций, зафиксированных до момента создания снимка.
Снимок данных текущей транзакции можно узнать с использованием функции pg_current_snapshot, например:
SELECT pg_current_snapshot();
pg_current_snapshot
---------------------
92628:92630:92628
(1 строка)
Функция pg_current_snapshot выдает снимок данных в формате xmin:xmax:xip_list.
В данном случае в снимок попали изменения транзакций с номерами, меньшими 92628 (не включая номер 92628, поскольку он содержится в xip_list), а также транзакция с номером 92629, поскольку она попала в диапазон xmin <= xid < xmax и не содержится в xip_list.
Построение снимка для различных уровней изоляции
Момент построения снимка зависит от используемого уровня изоляции транзакции.
На уровне изоляции Read Committed снимок строится в начале каждого оператора транзакции и используется на протяжении его выполнения:

Важным исключением из этого правила является использование функций с категорией изменчивости VOLATILE внутри оператора SQL.
В этом случае снимок строится не только в начале оператора, но и при каждом вызове изменчивой функции внутри оператора, что может нарушить согласованность данных даже в рамках одного запроса.
Более подробно построение снимков при использовании изменчивых функций будет рассмотрено в лабораторной работе.
На уровнях изоляции Repeatable Read и Serializable снимок строится один раз в начале первого оператора транзакции и используется на протяжении всей транзакции:

Правила видимости при использовании курсора
В соответствии с базовым правилом видимости собственные изменения внутри транзакции должны быть видны. Однако при использовании курсора собственные изменения, сделанные после его открытия, в самом курсоре видны быть не должны.
Для реализации указанного правила в заголовке версии строки дополнительно указывается следующая информация:
cmin— номер команды внутри транзакции, добавившей строкуcmax— номер команды внутри транзакции, удалившей строку
При открытии курсора создается снимок данных, в котором учитывается информация о текущем номере команды внутри транзакции. В дальнейшем при применении команды FETCH в транзакции используется созданный снимок.
Построение отдельных снимков для курсоров осуществляется на всех уровнях изоляции транзакций, включая Repeatable Read и Serializable.
Рассмотрим пример работы правил видимости при использовании курсора. Начнем транзакцию и вставим в пустую таблицу несколько строк:
test_db=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
test_db=*> INSERT INTO some_table VALUES (1), (2), (3);
INSERT 0 3
В данном случае транзакция начата с уровнем изоляции Repeatable Read с целью демонстрации того, что в случае использования курсора нарушается правило построения только одного снимка в транзакции с уровнем изоляции Repeatable Read и Serializable.
Определим и откроем командой DECLARE курсор, который получает все строки таблицы с указанием параметра cmax:
test_db=*> DECLARE c_some_table CURSOR FOR SELECT *, cmax FROM some_table;
DECLARE CURSOR
Последовательно удалим все строки таблицы и убедимся, что результаты удаления видны в транзакции при использовании команды SELECT:
test_db=*> DELETE FROM some_table WHERE id=1;
DELETE 1
test_db=*> DELETE FROM some_table WHERE id=2;
DELETE 1
test_db=*> DELETE FROM some_table WHERE id=3;
DELETE 1
test_db=*> SELECT * FROM some_table;
id
----
(0 строк)
И наконец, проверим, какие данные видит курсор:
test_db=*> FETCH ALL FROM c_some_table;
id | cmax
----+------
1 | 0
2 | 1
3 | 2
(3 строки)
Курсор все еще видит удаленные данные, так как снимок был построен при его открытии до удаления строк.
При этом в поле cmax записаны номера команд в транзакции, которые удаляли строки.
Горизонт транзакции
При работе механизма MVCC накапливаются устаревшие (неактуальные) версии строк, которые не нужны ни одной из активных транзакций. Такие версии строк называются «мертвыми» и должны быть очищены группой процессов под общим названием autovacuum (см. лекцию «Очистка»).
В свою очередь, транзакциям нужны устаревшие версии строк в следующих случаях:
- в транзакции есть активный снимок, в котором они видны
- в транзакции нет активного снимка, но были сделаны изменения, которые могут быть отменены
Факт того, что в снимке могут быть видны неактуальные версии строк, следует из его определения.
При этом xmin снимка — номер самой ранней активной транзакции на момент построения снимка — определяет его горизонт.
Это означает, что все транзакции с меньшими номерами в момент построения снимка были зафиксированы и удаленные ими версии строк в снимке не видны.
Однако неактуальные версии строк, удаленные транзакциями с xid >= xmin, могут быть еще нужны снимку.
В этом случае горизонт транзакции определяется горизонтом снимка.

На рисунке транзакция, создавшая снимок, закрашена синим цветом.
В то же время в транзакциях с уровнем изоляции Read Committed между операторами активные снимки данных отсутствуют. Тем не менее даже в такие моменты транзакции также могут быть нужны неактуальные версии строк. Они могут потребоваться на случай отката, если в транзакции были выполнены изменения.
В этом случае горизонт транзакции определяется ее номером.
Стоит отметить, что номер транзакции xid, о котором до этого шла речь, выдается транзакции только в момент первого изменения ею данных.
До этого транзакция обладает виртуальным номером virtual xid. Виртуальный номер не записывается в страницы данных (в полях xmin и xmax заголовка версии строки) и, соответственно, никак не учитывается в снимках данных.
Виртуальные номера используются с целью экономии ограниченного количества номеров реальных транзакций. Их использование возможно, поскольку читающие транзакции никак не влияют на видимость данных.
Пример удержания горизонта номером транзакции представлен ниже.

Таким образом, горизонтом транзакции выступает либо горизонт ее активного снимка, то есть значение xmin снимка, либо реальный номер транзакции xid в случае, если активный снимок отсутствует, но транзакция внесла изменения.
При этом читающая транзакция на уровне изоляции Read Committed в моменты отсутствия активного снимка горизонт не удерживает.
Горизонт транзакции можно узнать с использованием системного представления pg_stat_activity. Для хранения горизонта в нем отведено поле backend_xmin в соответствии с pid обслуживающего процесса.
Например, горизонт текущей транзакции можно узнать следующим образом:
test_db=*> SELECT pid, backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
pid | backend_xmin
-------+--------------
46076 | 92628
(1 строка)
Горизонт очистки
Горизонт самой старой активной транзакции определяет горизонт базы данных или горизонт очистки.
Действительно, изменения транзакций с меньшими номерами, безусловно, видны, а значит, неактуальные версии строк за горизонтом базы данных ни одной из активных транзакций не нужны и могут быть очищены.
Горизонт очистки — крайне важное понятие. Горизонтом определяется набор неактуальных версий строк, которые могут быть очищены, что влияет не только на объем хранимых данных, но и на эффективность их обработки.
Одна «длинная» транзакция, удерживающая свой горизонт, удерживает горизонт очистки, не позволяя удалять неактуальные версии строк. При этом указанные версии строк транзакции могут быть даже не нужны.
Поэтому так важно следить за ходом выполнения транзакций на уровне приложения. Одна даже бездействующая транзакция (состояние idle in transaction в pg_stat_activity) может удерживать горизонт очистки.
В случае, если длинные бездействующие транзакции все же возникают, время выполнения бездействующей транзакции можно ограничить параметром idle_in_transaction_session_timeout.
Помимо этого, может быть ограничено максимальное время жизни снимка данных. Для этого используется параметр old_snapshot_threshold.
Стоит отметить, что горизонт очистки определяется не только транзакциями в обслуживающих процессах.
В определении горизонта очистки участвуют также подготовленные транзакции, используемые в распределенных системах, а также транзакции, выполняющиеся на физических репликах в режиме обратной связи (hot_standby_feedback=on) для удержания снимка на мастере.
Итоги
Итак, вы узнали, что:
- Снимок определяет согласованную картину данных на определенный момент времени
- Снимок — это не физическая копия данных, а набор параметров, определяющих видимость версий строк в нем
- Момент построения снимка зависит от уровня изоляции транзакции
- Активный снимок или реальный номер транзакции определяют ее горизонт, ограничивающий возможность очистки неактуальных версий строк
Самопроверка
Вопрос 1
Что из перечисленного определяет базовое правило видимости версии строки в снимке? Выберите все верные варианты ответа:
Вопрос 2
Что из перечисленного определяет момент создания снимка данных?
Вопрос 3
Что из перечисленного может определять горизонт транзакции с уровнем изоляции Read Committed? Выберите все верные варианты ответа