Многоверсионность
Страницы и версии строк
16
Авторские права
© Postgres Professional, 2016–2025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Структура страниц и версий строк
Как работают операции над данными
Вложенные транзакции
3
Структура страниц
заголовок
страницы
указатели на
версии строк
заголовок
версии строки
специальная
область
версия
строки
табличная страница страница индекса
– 4 байта
Размер страницы составляет 8 Кбайт. Это значение можно увеличить
(вплоть до 32 Кбайт), но только при сборке. И таблицы, и индексы,
и большинство других объектов, которые в PostgreSQL обозначаются
термином relation, используют одинаковую структуру страниц, чтобы
пользоваться общим буферным кешем. В начале страницы идет
заголовок (24 байта), содержащий общие сведения о странице
и размер ее областей: указателей, свободного пространства, версий
строк и специальной области.
Версии строк содержат те самые данные, которые мы храним в
таблицах и других объектах БД, плюс заголовок. «Версия строки»
по-английски называется tuple; иногда мы будем говорить просто
«строка».
Указатели имеют фиксированный размер (4 байта) и составляют
массив, позиция в котором определяет идентификатор строки (tuple id,
tid). Указатели ссылаются на версии строк (tuple), расположенные в
конце блока. Такая косвенная адресация удобна тем, что во-первых,
позволяет найти нужную строку, не перебирая все содержимое блока
(строки имеют разную длину), а во-вторых, позволяет перемещать
строку внутри блока, не ломая ссылки из индексов
Между указателями и версиями строк находится свободное место.
Для некоторых типов индексов нужно хранить служебную информацию;
для этого может использоваться нулевая страница, а также
специальная область в конце каждой страницы.
4
Формат данных
Страницы читаются в оперативную память «как есть»
данные не переносимы между разными платформами
между полями данных возможны пропуски из-за выравнивания
Формат данных на диске полностью совпадает с представлением
данных в оперативной памяти. Страница читается в буферный кеш
«как есть», без преобразований.
Поэтому файлы данных на одной платформе (разрядность, порядок
байтов и т. п.) оказываются несовместимыми с другими платформами.
Кроме того, многие архитектуры предусматривают выравнивание
данных по границам машинных слов. Например, на 32-битной системе
x86 целые числа (тип integer, занимает 4 байта) будут выровнены по
границе 4-байтных слов, как и числа с плавающей точкой двойной
точности (тип double precision, 8 байт). А в 64-битной системе значения
double precision будут выровнены по границе 8-байтных слов.
Из-за этого размер табличной строки зависит от порядка расположения
полей. Обычно этот эффект не сильно заметен, но в некоторых случаях
он может привести к существенному увеличению размера. Например,
если располагать поля типов char(1) и integer вперемежку, между ними,
как правило, будет пропадать 3 байта.
6
указатель
Указатели на версии строк
версия строки
ссылка
на версию строки,
ее длина и статус
Страница содержит массив указателей на версии строк.
Каждый указатель (занимающий 4 байта) содержит:
ссылку на версию строки;
длину этой версии строки (для удобства);
несколько бит, определяющих статус версии строки.
7
ctidxmin xmax данные
xmin committed
xmin aborted
xmax committed
xmax aborted
heap hot upd
heap only tuple
Версии строк в таблице
(0,2)
(0,2)
номер транзакции,
создавшей версию
номер транзакции,
удалившей версию
infomask — различные
информационные биты
следующая версия
той же строки
значения полей
табличной строки
указатель
Версии строк (tuples) в табличных страницах (heap pages) кроме
собственно данных также имеют заголовок. Этот заголовок, помимо
прочего, содержит следующие важные поля:
xmin и xmax определяют видимость данной версии строки
в терминах начального и конечного номеров транзакций. Номера
транзакций (TransactionId или xid) последовательно выбираются для
транзакций из глобального счетчика, который используется всеми
базами данных в рамках кластера PostgreSQL.
infomask содержит ряд битов, определяющих свойства данной
версии. На рисунке показаны основные из них, но далеко не все.
Часть показанных битов будет рассмотрена в этой теме, часть —
в других темах этого модуля.
ctid является ссылкой на следующую, более новую, версию той же
строки. У самой новой, актуальной, версии строки ctid ссылается
на саму эту версию. Такие ссылки используются не всегда, мы
рассмотрим их в теме «HOT-обновления».
Заголовок версии строки на табличной странице составляет 23 байта
(или больше: в него включается битовая карта неопределенных
значений).
Напомним, что каждая версия строки всегда целиком помещается
внутрь одной страницы. Если версия строки имеет большой размер,
PostgreSQL попробует сжать часть полей или вынести часть полей
во внешнее TOAST-хранилище (это рассматривается в модуле
«Организация данных» курса DBA1).
8
ctid ключ
Индексные записи
ctidxmin xmax данные
xmin committed
xmin aborted
xmax committed
xmax aborted
heap hot upd
heap only tuple
(0,2)
(0,1)
указатель
(0,2)(0,2)
(0,1)
(0,2)
указатель на версию
строки в таблице
значения ключей
индексирования
Информация в индексной странице сильно зависит от типа индекса.
И даже у одного типа индекса бывают разные виды страниц. Например,
у B-дерева есть страница с метаданными и «обычные» страницы.
Тем не менее, обычно в странице имеется массив указателей и строки
(так же, как и в табличной странице). Во избежание путаницы мы будем
называть индексные строки записями. Кроме того, в конце индексной
страницы отводится место под специальные данные.
Сами индексные записи тоже могут иметь очень разную структуру
в зависимости от типа индекса. Например, для B-дерева записи,
относящиеся к листовым страницам, содержат значение ключа
индексирования и идентификатор (ctid) строки таблицы (подробно
структура B-дерева и другие индексы разбираются в учебном курсе
QPT «Оптимизация запросов»).
Устройство индексов других типов может быть разным, но, как правило,
страницы индекса все равно содержат ссылки на версии строк.
Идентификаторы ctid имеют вид (x,y): здесь x — номер страницы, y
порядковый номер указателя в массиве. Для удобства мы будем
показывать их слева от указателей на табличные версии строк.
Никакой индекс не содержит информацию о версионности (в нем нет
полей xmin и xmax). Прочитав только индексную запись, невозможно
определить видимость строки, на которую она ссылается: приходится
обращаться к табличной странице или карте видимости.
На рисунке показаны записи листовых страниц обычного индекса
B-дерева. Для простоты указатели на эти записи опущены.
9
Вставка
xmin xmax данные
100 0 t 42,FOO
ctid ключ
100
committed
aborted
(0,1)
указатель
normal
(0,1) FOO
xmin committed
xmin aborted
xmax committed
xmax aborted
CLOG
Рассмотрим, как выполняются операции со строками на низком уровне,
и начнем со вставки.
В нашем примере предполагается таблица с двумя столбцами
(числовой и текстовый); по текстовому полю создан индекс B-дерево.
При вставке строки в табличной странице появится указатель
с номером 1, ссылающийся на первую и единственную версию строки.
В версии строки поле xmin заполнено номером текущей транзакции
(100 в нашем примере).
В журнале статусов транзакций (CLOG) хранятся состояния всех
транзакций (начиная с некоторой), для каждой транзакции отведено два
бита. Эти данные хранятся в каталоге PGDATA/pg_xact, а несколько
наиболее актуальных страниц кешируются в разделяемой памяти
сервера. Поскольку в нашем примере транзакция 100 еще активна, оба
бита пока сброшены.
В индексной странице также создается указатель с номером 1, который
ссылается на индексную запись, которая, в свою очередь, ссылается на
первую версию строки в табличной странице. Чтобы не загромождать
рисунок, указатель и индексная запись объединены.
Поле xmax заполнено фиктивным номером 0, поскольку данная версия
строки не удалена и является актуальной. Транзакции не будут
обращать внимание на этот номер, поскольку установлен бит xmax
aborted.
11
xmin committed
xmin aborted
xmax committed
xmax aborted
Фиксация изменений
xmin xmax данные
100 0 t 42,FOO
ctid ключ
100
committed
aborted
(0,1)
указатель
normal
(0,1) FOO
t
при первом
обращении к строке
другой транзакцией
t
При фиксации изменений в CLOG для данной транзакции выставляется
признак committed. Это, по сути, единственная операция (не считая
журнала упреждающей записи), которая необходима.
Когда какая-либо другая транзакция обратится к этой табличной
странице, ей придется ответить на вопросы:
1) завершилась ли транзакция 100 (надо проверить список активных
транзакций — такая структура поддерживается в общей памяти
сервера),
2) а если завершилась, то фиксацией или отменой (свериться с CLOG).
Поскольку выполнять проверку по CLOG каждый раз накладно,
выясненный однажды статус транзакции записывается в биты-
подсказки xmin committed и xmin aborted. Если один из этих битов
установлен, то состояние транзакции xmin считается известным и
следующей транзакции уже не придется обращаться к CLOG.
Почему эти биты не устанавливаются той транзакцией, которая
выполняла вставку? В момент, когда транзакция фиксируется или
отменяется, уже непонятно, какие именно строки в каких именно
страницах транзакция успела поменять. Кроме того, часть этих страниц
может быть вытеснена из буферного кеша на диск; читать их заново,
чтобы изменить биты, означало бы существенно замедлить фиксацию.
Обратная сторона состоит в том, что любая транзакция (даже
выполняющая простое чтение — SELECT) может загрязнить данные
в буферном кеше и породить новые журнальные записи.
13
xmin committed
xmin aborted
xmax committed
xmax aborted
t
Удаление
xmin xmax данные
100 42,FOO
ctid ключ
100
committed
aborted
(0,1)
указатель
normal
(0,1) FOO
t
101
101
выступает
блокировкой
строки
При удалении строки в поле xmax текущей версии записывается номер
текущей удаляющей транзакции, а бит xmax aborted сбрасывается.
Больше ничего не происходит.
Заметим, что установленное значение xmax, соответствующее
активной транзакции (что определяется другими транзакциями по
списку активных), выступает в качестве признака блокировки. Если
другая транзакция намерена обновить или удалить эту строку, она
будет вынуждена дождаться завершения транзакции xmax.
Подробнее блокировки рассматриваются в одноименном модуле.
Пока отметим только, что число блокировок строк ничем не ограничено.
Они не занимают место в оперативной памяти и производительность
системы не страдает от их количества (разумеется, за исключением
того, что первый процесс, обратившийся к странице, должен будет
проставить биты-подсказки).
15
xmin committed
xmin aborted
xmax committed
xmax aborted
Отмена изменений
t
xmin данные
100 42,FOO
ctid ключ
100
committed
aborted
(0,1)
указатель
normal
(0,1) FOO
t
101
101 t
t
при первом
обращении
к строке
остается
номер отмененной
транзакции
xmax
Отмена изменений работает аналогично фиксации, только в CLOG
для транзакции выставляется бит aborted. Отмена выполняется так же
быстро, как и фиксация — не требуется выполнять откат выполненных
действий.
Номер прерванной транзакции остается в поле xmax — его можно было
бы стереть, но в этом нет смысла. При обращении к странице будет
проверен статус и в версию строки будет установлен бит подсказки
xmax aborted. Это будет означать, что на поле xmax смотреть не нужно.
17
Обновление
xmin committed
xmin aborted
xmax committed
xmax aborted
t
xmin данные
100 42,FOO
ctid ключ
100
committed
aborted
(0,1)
указатель
normal
(0,2) BAR
t
101 t
normal(0,2)
(0,1) FOO
102
102 42,BAR0 t
102
перезаписан
номер отмененной
транзакции
xmax
ссылки
на обе версии
строки
Обновление работает так, как будто сначала выполнялось удаление
старой версии строки, а затем вставка новой.
Старая версия помечается номером текущей транзакции в поле xmax.
Обратите внимание, что новое значение 102 записалось поверх старого
101, так как транзакция 101 была отменена. Кроме того, биты xmax
committed и xmax aborted старой версии строки сброшены, так как
статус текущей транзакции еще не известен.
В индексной странице появляется второй указатель и вторая запись,
ссылающаяся на вторую версию в табличной странице.
Так же, как и при удалении, значение xmax в первой версии строки
служит признаком того, что строка заблокирована.
19
Точка сохранения
Возможность откатить часть транзакции
BEGIN;
INSERT INTO t(n) VALUES (42);
SAVEPOINT SP;
DELETE FROM t;
ROLLBACK TO SP;
UPDATE t SET n = n + 1;
COMMIT;
txid = 101
txid = 102
txid = 100
100
committed
aborted
t
101 t
102
t
вложенные
транзакции
основная
транзакция
Тонкий момент представляет функционал точек сохранения,
позволяющий отменить часть операций текущей транзакции. Это не
укладывается в приведенную выше схему, поскольку физически
никакие данные не откатываются, а лишь изменяется статус всей
транзакции целиком.
Поэтому транзакция с точкой сохранения состоит из отдельных
вложенных (не путать с автономными!) транзакций (subtransactions),
статусом которых можно управлять отдельно.
20
Вложенные транзакции
Собственный номер и статус в CLOG
конечный статус зависит от статуса основной транзакции
Информация о вложенности сохраняется на диске
каталог PGDATA/pg_subtrans
данные кешируются в буферах общей памяти (аналогично CLOG)
Примеры использования
точка сохранения SAVEPOINT
обработка исключений в PL/pgSQL (EXCEPTION)
режим psql ON_ERROR_ROLLBACK = on/interactive
Вложенные транзакции имеют свой номер (бóльший, чем номер
основной транзакции). Статус вложенных транзакций записывается
обычным образом в CLOG, однако финальный статус зависит от
статуса основной транзакции: если она отменена, то отменяются также
и все вложенные транзакции.
Информация о вложенности транзакций хранится в каталоге
PGDATA/pg_subtrans. Обращение к файлам происходит через буферы
в общей памяти сервера, организованные так же, как и буферы CLOG.
Вложенные транзакции нельзя использовать явно, то есть нельзя
начать новую транзакцию, не завершив текущую. Этот механизм
задействуется неявно при использовании точек сохранения, при
обработке исключений PL/pgSQL и т. п.
Особенный интерес представляет режим ON_ERROR_ROLLBACK
в psql, при включении которого транзакция, выполнившая ошибочную
операцию, не прерывается, а продолжает работать. Почему этот режим
не включен по умолчанию? Дело в том, что ошибка может произойти
где-то в середине выполнения оператора, и таким образом нарушится
атомарность выполнения оператора. Единственный способ отменить
изменения, уже сделанные этим оператором, не трогая остальные
изменения — использовать вложенные транзакции. Поэтому режим
ON_ERROR_ROLLBACK фактически ставит перед каждой командой
неявную точку сохранения. А это чревато существенными накладными
расходами.
22
Итоги
В табличных страницах может храниться несколько версий
одной и той же строки, ограниченных номерами транзакций
xmin и xmax
В индексных записях нет информации о версионности
Фиксация и откат выполняются одинаково быстро
Для точек сохранения используются вложенные транзакции
23
Практика
1. Создайте таблицу и вставьте в нее одну строку. Затем
дважды обновите эту строку и удалите ее. Сколько версий
строк находится сейчас в таблице?
Проверьте, используя расширение pageinspect.
2. Определите, в какой странице находится строка таблицы
pg_class, относящаяся к самой таблице pg_class. Сколько
актуальных версий строк находится в той же странице?
3. Включите в psql параметр ON_ERROR_ROLLBACK
и убедитесь, что этот режим использует вложенные
транзакции.