02 Занятие 02: Многоверсионность / Страницы и версии строк
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba2_02_mvcc_tuples.html Archive: DBA2-handouts-16.zip Страниц PDF: 27
Page 1: Многоверсионность / Страницы и версии строк
Авторские права
© Postgres Professional, 2016–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Структура страниц и версий строк
- Как работают операции над данными
- Вложенные транзакции
Page 3: Структура страниц
{#source-figure-003 caption=“Source slide page 3: Структура страниц”}
Комментарии преподавателя
Размер страницы составляет 8 Кбайт. Это значение можно увеличить (вплоть до 32 Кбайт), но только при сборке. И таблицы, и индексы, и большинство других объектов, которые в PostgreSQL обозначаются термином relation, используют одинаковую структуру страниц, чтобы пользоваться общим буферным кешем. В начале страницы идет заголовок (24 байта), содержащий общие сведения о странице и размер ее областей: указателей, свободного пространства, версий строк и специальной области.
Версии строк содержат те самые данные, которые мы храним в таблицах и других объектах БД, плюс заголовок. «Версия строки» по-английски называется tuple; иногда мы будем говорить просто «строка».
Указатели имеют фиксированный размер (4 байта) и составляют массив, позиция в котором определяет идентификатор строки (tuple id, tid). Указатели ссылаются на версии строк (tuple), расположенные в конце блока. Такая косвенная адресация удобна тем, что во-первых, позволяет найти нужную строку, не перебирая все содержимое блока (строки имеют разную длину), а во-вторых, позволяет перемещать строку внутри блока, не ломая ссылки из индексов
Между указателями и версиями строк находится свободное место.
Для некоторых типов индексов нужно хранить служебную информацию; для этого может использоваться нулевая страница, а также специальная область в конце каждой страницы.
https://postgrespro.ru/docs/postgresql/16/storage-page-layout
Page 4: Формат данных
{#source-figure-004 caption=“Source slide page 4: Формат данных”}
Страницы читаются в оперативную память «как есть»
- данные не переносимы между разными платформами
- между полями данных возможны пропуски из-за выравнивания
Комментарии преподавателя
Формат данных на диске полностью совпадает с представлением данных в оперативной памяти. Страница записывается в файл и читается в буферный кеш «как есть», без преобразований.
Поэтому файлы данных на одной платформе (разрядность, порядок байтов и т. п.) оказываются несовместимыми с другими платформами.
Кроме того, многие архитектуры предусматривают выравнивание данных по границам машинных слов. Например, на 32-битной системе x86 целые числа (тип integer, занимает 4 байта) будут выровнены по границе 4-байтных слов, как и числа с плавающей точкой двойной точности (тип double precision, 8 байт). А в 64-битной системе значения double precision будут выровнены по границе 8-байтных слов.
Из-за этого размер табличной строки зависит от порядка расположения и типов полей. Обычно этот эффект не сильно заметен, но в некоторых случаях он может привести к существенному увеличению размера. Например, если располагать поля типов boolean и integer вперемежку, между ними, как правило, будет пропадать 3 байта.
https://pgconf.ru/rmedia/2016/05/13/tuple-internals-ru.pdf
Page 5: Структура страниц
Для изучения структуры и содержания страниц предназначено расширение pageinspect.
Границы областей страницы записаны в ее заголовке. Возьмем для примера нулевую страницу одной из таблиц системного каталога:
Области занимают следующие диапазоны адресов: 0 — начало заголовка страницы и указатели на версии строк, lower — начало свободного места, upper — начало данных (версий строк), special — начало спец. данных (только для индексов), pagesize — конец страницы.
Page 6: Указатели на версии строк
{#source-figure-006 caption=“Source slide page 6: Указатели на версии строк”}
Комментарии преподавателя
Страница содержит массив указателей на версии строк.
Каждый указатель (занимающий 4 байта) содержит:
● ссылку на версию строки;
● длину этой версии строки (для удобства);
● несколько бит, определяющих статус версии строки.
Page 7: Версии строк в таблице
{#source-figure-007 caption=“Source slide page 7: Версии строк в таблице”}
Комментарии преподавателя
Версии строк (tuples) в табличных страницах (heap pages) кроме собственно данных также имеют заголовок. Этот заголовок, помимо прочего, содержит следующие важные поля:
● xmin и xmax определяют видимость данной версии строки в терминах начального и конечного номеров транзакций. Номера транзакций (TransactionId или xid) последовательно выбираются для транзакций из глобального счетчика, который используется всеми базами данных в рамках кластера PostgreSQL.
● infomask содержит ряд битов, определяющих свойства данной версии. На рисунке показаны основные из них, но далеко не все. Часть показанных битов будет рассмотрена в этой теме, часть — в других темах этого модуля.
● ctid является ссылкой на следующую, более новую, версию той же строки. У самой новой, актуальной, версии строки ctid ссылается на саму эту версию. Такие ссылки используются не всегда, мы рассмотрим их в теме «HOT-обновления и самоочистка».
Заголовок версии строки на табличной странице составляет 23 байта (или больше: в него включается битовая карта неопределенных значений).
Напомним, что каждая версия строки всегда целиком помещается внутрь одной страницы. Если версия строки имеет большой размер, PostgreSQL попробует сжать часть полей или вынести часть полей во внешнее TOAST-хранилище (это рассматривается в модуле «Организация данных» курса DBA1).
Page 8: ctid ключ Индексные записи
{#source-figure-008 caption=“Source slide page 8: ctid ключ Индексные записи”}
Комментарии преподавателя
Информация в индексной странице сильно зависит от типа индекса. И даже у одного типа индекса бывают разные виды страниц. Например, у B-дерева есть страница с метаданными и «обычные» страницы.
Тем не менее, обычно в странице имеется массив указателей и строки (так же, как и в табличной странице). Во избежание путаницы мы будем называть индексные строки записями. Кроме того, в конце индексной страницы отводится место под специальные данные.
Сами индексные записи тоже могут иметь очень разную структуру в зависимости от типа индекса. Например, для B-дерева записи, относящиеся к листовым страницам, содержат значение ключа индексирования и идентификатор (ctid) строки таблицы (подробно структура B-дерева и другие индексы разбираются в учебном курсе QPT «Оптимизация запросов»).
Устройство индексов других типов может быть разным, но, как правило, страницы индекса все равно содержат ссылки на версии строк.
Идентификаторы ctid имеют вид (x,y): здесь x — номер страницы, y — порядковый номер указателя в массиве. Для удобства мы будем показывать их слева от указателей на табличные версии строк.
Никакой индекс не содержит информацию о версионности (в нем нет полей xmin и xmax). Прочитав только индексную запись, невозможно определить видимость строки, на которую она ссылается: приходится обращаться к табличной странице или карте видимости.
На рисунке показаны записи листовых страниц обычного индекса — B-дерева. Для простоты указатели на эти записи опущены.
Page 9: Вставка
{#source-figure-009 caption=“Source slide page 9: Вставка”}
Комментарии преподавателя
Рассмотрим, как выполняются операции со строками на низком уровне, и начнем со вставки.
В нашем примере предполагается таблица с двумя столбцами (числовой и текстовый); по текстовому полю создан индекс B-дерево.
При вставке строки в табличной странице появится указатель с номером 1, ссылающийся на первую и единственную версию строки. В версии строки поле xmin заполнено номером текущей транзакции (100 в нашем примере).
В журнале статусов транзакций (CLOG) хранятся состояния всех транзакций (начиная с некоторой), для каждой транзакции отведено два бита. Эти данные хранятся в каталоге PGDATA/pg_xact, а несколько наиболее актуальных страниц кешируются в разделяемой памяти сервера. Поскольку в нашем примере транзакция 100 еще активна, оба бита пока сброшены.
В индексной странице также создается указатель с номером 1, который ссылается на индексную запись, которая, в свою очередь, ссылается на первую версию строки в табличной странице. Чтобы не загромождать рисунок, указатель на индексную запись не показан.
Поле xmax заполнено фиктивным номером 0, поскольку данная версия строки не удалена и является актуальной. Транзакции не будут обращать внимание на этот номер, поскольку установлен бит xmax aborted.
Page 10: Вставка
Создадим таблицу и индекс:
Для удобства создадим представление, которое с помощью расширения pageinspect покажет интересующую нас информацию о версиях строк из нулевой страницы таблицы:
Также создадим представление, чтобы заглянуть в индекс. Нулевая страница индекса содержит метаинформацию, поэтому смотрим в первую:
Вставим одну строку, предварительно начав транзакцию.
Вот номер нашей текущей транзакции и ее статус:
Вот что содержится в табличной странице:
Page 11: Page 11
Похожую, но существенно менее детальную информацию можно получить и из самой таблицы, используя псевдостолбцы ctid, xmin и xmax:
В индексной странице видим один указатель на единственную строку таблицы:
Page 12: Фиксация изменений
{#source-figure-012 caption=“Source slide page 12: Фиксация изменений”}
Комментарии преподавателя
При фиксации изменений в CLOG для данной транзакции выставляется признак committed. Это, по сути, единственная операция (не считая журнала упреждающей записи), которая необходима.
Когда какая-либо другая транзакция обратится к этой версии строки, ей придется ответить на вопросы:
завершилась ли транзакция 100 (надо проверить список активных транзакций — такая структура поддерживается в общей памяти сервера),
а если завершилась, то фиксацией или отменой (свериться с CLOG).
Поскольку выполнять проверку по CLOG каждый раз накладно, выясненный однажды статус транзакции записывается в биты-подсказки xmin committed и xmin aborted. Если один из этих битов установлен, то состояние транзакции xmin считается известным и следующей транзакции уже не придется обращаться к CLOG.
Почему эти биты не устанавливаются той транзакцией, которая выполняла вставку? В момент, когда транзакция фиксируется или отменяется, уже непонятно, какие именно строки в каких именно страницах транзакция успела поменять. Кроме того, часть этих страниц может быть вытеснена из буферного кеша на диск; читать их заново, чтобы изменить биты, означало бы существенно замедлить фиксацию.
Обратная сторона состоит в том, что любая транзакция (даже выполняющая простое чтение — SELECT) может загрязнить данные в буферном кеше и породить новые журнальные записи.
Page 13: Фиксация изменений
Выполним фиксацию:
Что изменилось в странице?
Ничего, так как единственная операция, которая выполняется при фиксации — запись статуса транзакции в CLOG.
Информация о статусах транзакций хранится в подкаталоге pg_xact каталога PGDATA и кешируется в общей памяти. Начиная с PostgreSQL 13, статистику использования кешей, в том числе кеша статусов транзакций, показывает представление pg_stat_slru:
Транзакция, первой обратившаяся к странице, должна будет определить статус транзакции xmin. Этот статус будет записан в информационные биты:
Page 14: Удаление
{#source-figure-014 caption=“Source slide page 14: Удаление”}
Комментарии преподавателя
При удалении строки в поле xmax текущей версии записывается номер удаляющей транзакции, а бит xmax aborted сбрасывается. Больше ничего не происходит.
Заметим, что установленное значение xmax, соответствующее активной транзакции (что определяется другими транзакциями по списку активных), выступает в качестве признака блокировки. Если другая транзакция намерена обновить или удалить эту строку, она будет вынуждена дождаться завершения транзакции xmax.
Подробнее блокировки рассматриваются в одноименном модуле. Пока отметим только, что число блокировок строк ничем не ограничено. Они не занимают место в оперативной памяти и производительность системы не страдает от их количества (разумеется, за исключением того, что первый процесс, обратившийся к странице, должен будет проставить биты-подсказки).
Page 15: Удаление
Теперь удалим строку.
Номер транзакции записался в поле xmax:
Page 16: Отмена изменений
{#source-figure-016 caption=“Source slide page 16: Отмена изменений”}
Комментарии преподавателя
Отмена изменений работает аналогично фиксации, только в CLOG для транзакции выставляется бит aborted. Отмена выполняется так же быстро, как и фиксация — не требуется выполнять откат выполненных действий.
Номер прерванной транзакции остается в поле xmax — его можно было бы стереть, но в этом нет смысла. При обращении к странице будет проверен статус и в версию строки будет установлен бит подсказки xmax aborted. Это будет означать, что на поле xmax смотреть не нужно.
Page 17: Отмена изменений
При обрыве транзакции номер xmax остается в заголовке.
А при первом обращении к странице выставляется соответствующий бит:
Page 18: Обновление
{#source-figure-018 caption=“Source slide page 18: Обновление”}
Комментарии преподавателя
Обновление работает так, как будто сначала выполнялось удаление старой версии строки, а затем вставка новой.
Старая версия помечается номером текущей транзакции в поле xmax. Обратите внимание, что новое значение 102 записалось поверх старого 101, так как транзакция 101 была отменена. Кроме того, биты xmax committed и xmax aborted старой версии строки сброшены, так как статус текущей транзакции еще не известен.
В индексной странице появляется второй указатель и вторая запись, ссылающаяся на вторую версию в табличной странице.
Так же, как и при удалении, значение xmax в первой версии строки служит признаком того, что строка заблокирована.
Page 19: Обновление
Теперь проверим обновление.
Запрос выдает одну строку (новую версию):
Но в странице мы видим обе версии. Причем новый номер транзакции записался на место старого (поскольку старая транзакция была отменена).
При этом в индексной странице обнаруживаем указатели на обе версии:
Индексные записи внутри страницы упорядочены по значению ключа. Поэтому первой идет запись с ключом BAR (ссылается на версию (0,2)), а второй — запись с ключом FOO (ссылается на версию (0,1)).
Page 20: Точка сохранения
{#source-figure-020 caption=“Source slide page 20: Точка сохранения”}
Возможность откатить часть транзакции
Комментарии преподавателя
Тонкий момент представляет функционал точек сохранения, позволяющий отменить часть операций текущей транзакции. Это не укладывается в приведенную выше схему, поскольку физически никакие данные не откатываются, а лишь изменяется статус всей транзакции целиком.
Поэтому транзакция с точкой сохранения состоит из отдельных вложенных (не путать с автономными!) транзакций (subtransactions), статусом которых можно управлять отдельно.
Page 21: Вложенные транзакции
Собственный номер и статус в 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 фактически ставит перед каждой командой неявную точку сохранения. А это чревато существенными накладными расходами.
Page 22: Точки сохранения и вложенные транзакции
Опустошим таблицу (при этом опустошаются файлы таблицы и индекса):
Начинаем транзакцию и вставляем строку.
Ставим точку сохранения и удаляем строку.
Обратите внимание: функция pg_current_xact_id() выдает номер основной, а не вложенной, транзакции:
Откатимся к точке сохранения. Версии строк в странице остаются на месте, но изменится статус вложенной транзакции:
Запрос к таблице снова покажет строку — ее удаление было отменено:
Для дальнейших изменений создается новая вложенная транзакция. Ее номер будет записан в поле xmax первой версии строки вместо номера отмененной вложенной транзакции 754 и в поле xmin новой версии:
Page 23: Page 23
Фиксируем изменения. При этом в таблице, как и прежде, ничего не меняется:
А в CLOG основная транзакция и все вложенные, которые еще не завершены, получают статус committed:
Информация о вложенности транзакций хранится в подкаталоге pg_subtrans каталога PGDATA. Она кешируется в общей памяти, как и информация о статусах транзакций:
Page 24: Итоги
В табличных страницах может храниться несколько версий одной и той же строки, ограниченных номерами транзакций xmin и xmax
- В индексных записях нет информации о версионности
- Фиксация и откат выполняются одинаково быстро
- Для точек сохранения используются вложенные транзакции
Page 25: Практика
- Создайте таблицу и вставьте в нее одну строку. Затем дважды обновите эту строку и удалите ее. Сколько версий строк находится сейчас в таблице? Проверьте, используя расширение pageinspect.
- Определите, в какой странице находится строка таблицы pg_class, относящаяся к самой таблице pg_class. Сколько актуальных версий строк находится в той же странице?
- Включите в psql параметр ON_ERROR_ROLLBACK и убедитесь, что этот режим использует вложенные транзакции.
Page 26: 1. Версии строк
Создаем расширение и таблицу.
Вставляем строку, обновляем ее и затем удаляем:
В таблице ничего нет:
Проверяем версии в странице:
2. Версии строк на определенной странице
Номер страницы содержится в первой компоненте значения ctid:
К сожалению, тип данных tid не позволяет непосредственно получить номер страницы, но можно, например,
Page 27: Page 27
воспользоваться приведением к типу point:
Количество строк на той же странице:
3. Режим ON\_ERROR\_ROLLBACK
Включим режим:
Начнем транзакцию и вставим строку.
Вставим еще одну строку.
Каждая команда происходит в отдельной вложенной транзакции, что и требовалось установить.
Установленный режим будет действовать вплоть до завершения сеанса psql. А чтобы эта настройка не помешала дальнейшей работе в этом же сеансе, вернем прежний режим: