Архитектура
Внутреннее устройство
12
Авторские права
© Postgres Professional, 2020 год.
Авторы: Егор Рогов, Павел Лузанов
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Страницы
Версии строк
Снимки данных
3
Страницы
Структура страниц
Формат данных
4
Страницы
заголовок
страницы
указатели на
версии строк
заголовок
версии строки
специальная
область
версия
строки
табличная страница страница индекса
– 4 байта
Размер страницы составляет 8 Кбайт. Это значение можно увеличить
(вплоть до 32 Кбайт), но только при сборке. И таблицы, и индексы,
и большинство других объектов, которые в PostgreSQL обозначаются
термином relation, используют одинаковую структуру страниц, чтобы
пользоваться общим буферным кешем. В начале страницы идет
заголовок (24 байта), содержащий общие сведения и размер областей
страницы: указателей, свободного пространства, версий строк и
специальной области.
Версии строк содержит те самые данные, которые мы храним
в таблицах и других объектах БД, плюс заголовок. «Версия строки» по-
английски называется tuple; иногда мы будем говорить просто «строка».
Указатели имеют фиксированный размер (4 байта) и составляют
массив, позиция в котором определяет идентификатор строки (tuple id,
tid). Указатели ссылаются на версии строк (tuple), расположенные
в конце блока. Такая косвенная адресация удобна тем, что во-первых,
позволяет найти нужную строку, не перебирая все содержимое блока
(строки имеют разную длину), а во-вторых, позволяет перемещать
строку внутри блока, не ломая ссылки из индексов.
Между указателями и версиями строк находится свободное место.
Некоторые типы индексов нуждаются в хранении служебной
информации; для этого они могут использовать специальную область
в конце страницы.
5
Формат данных
Страницы читаются в оперативную память «как есть»
данные не переносимы между разными платформами
между полями данных возможны пропуски из-за выравнивания
Формат данных на диске полностью совпадает с представлением
данных в оперативной памяти. Страница читается в буферный кеш
«как есть», без преобразований.
Поэтому файлы данных на одной платформе (разрядность, порядок
байтов и т. п.) оказываются несовместимыми с другими платформами.
Кроме того, многие архитектуры предусматривают выравнивание
данных по границам машинных слов. Например, на 32-битной системе
x86 целые числа (тип integer, занимает 4 байта) будут выровнены по
границе 4-байтных слов, как и числа с плавающей точкой двойной
точности (тип double precision, 8 байт). А в 64-битной системе значения
double precision будут выровнены по границе 8-байтных слов.
Из-за этого размер табличной строки зависит от порядка расположения
полей. Обычно этот эффект не сильно заметен, но в некоторых случаях
он может привести к существенному увеличению размера. Например,
если располагать поля типов char(1) и integer вперемешку, между ними,
как правило, будет пропадать 3 байта.
7
Версии строк
Структура версий строк
Как работают операции над данными
HOT-обновления
8
100
Вставка
xmin xmax данные
100 0 42,FOO
committed
aborted
XACT
номер транзакции,
создавшей версию
номер транзакции,
удалившей версию
значения полей
табличной строки
версия строки
в таблице
Рассмотрим, как устроены версии строк (tuples) и как выполняются
операции со строками на низком уровне. Начнем со вставки.
В нашем примере — таблица с двумя столбцами (число и текст).
При вставке строки в табличной странице (heap page) появится
указатель с номером 1, ссылающийся на первую и единственную
версию строки. Каждый такой указатель помимо ссылки содержит длину
версии строки и несколько бит, определяющих ее статус.
Версии строк кроме собственно данных имеют также заголовок,
занимающий минимум 23 байта. Помимо прочего он содержит:
- xmin и xmax — поля, определяющие видимость данной версии строки
в терминах начального и конечного номеров транзакций;
- карту неопределенных значений (в ней отмечены поля, равные NULL);
- ряд признаков, показывающих, например, статус транзакций xmin и
xmax, если он уже известен (зафиксированы или оборваны).
В нашей версии строки поле xmin заполнено номером текущей
транзакции (100). Поскольку изменения еще не фиксировались и
транзакция активна, то в журнале статуса транзакций (XACT)
соответствующая запись заполнена нулями. XACT можно представить
себе как массив, в котором для каждой транзакции (начиная
с некоторой) отводится ровно два бита.
Поле xmax заполнено фиктивным номером 0. Транзакции не будут
обращать внимание на этот номер, поскольку установлен признак, что
эта транзакция оборвана.
9
100
Вставка
xmin xmax данные
100 0 42,FOO
ctid ключ
committed
aborted
(0,1) FOO
XACT
указатель на версию
строки в таблице
значения ключей
индексирования
строка
индекса
версия строки
в таблице
индекс не содержит информации о версионности
Пусть также имеется индекс B-дерево, созданный по текстовому полю.
Информация в индексной странице сильно зависит от типа индекса.
И даже у одного типа индекса бывают разные виды страниц. Например,
у B-дерева есть страница с метаданными и «обычные» страницы.
Тем не менее, обычно в странице имеется массив указателей и строки
(так же, как и в табличной странице).
Строки в индексах тоже могут иметь очень разную структуру
в зависимости от типа индекса. Например, для B-дерева строки,
относящиеся к листовым страницам, содержат значение ключа
индексирования и ссылку (ctid) на соответствующую строку таблицы
(структура B-дерева разбирается в теме «Классы операторов» и
в учебном курсе QPT «Оптимизация запросов»).
В общем случае индекс может быть устроен совсем другим образом, но
как правило он все равно будет содержать ссылки на версии строк.
В нашем примере в индексной странице также создается указатель
с номером 1, который ссылается на индексную строку, которая, в свою
очередь, ссылается на первую строку в табличной странице. Чтобы не
загромождать рисунок, указатель и строка объединены.
Важный момент состоит в том, что никакой индекс не содержит
информацию о версионности (нет полей xmin и xmax). Прочитав строку
индекса, невозможно определить видимость этой строки, не заглянув
в табличную страницу (для оптимизации служит карта видимости).
10
данныеxmaxxmin
100
committed
aborted
0
Фиксация изменений
при первом
обращении к строке
другой транзакцией
100 42,FOO
ctid ключ
(0,1) FOO
(0,1)
t
Когда транзакция фиксируется, в XACT для этой транзакции
выставляется признак committed. Это, по сути, единственная операция
(не считая журнала предзаписи), которая необходима.
Когда какая-либо другая транзакция обратится к версии строки, ей
придется ответить на вопросы:
1. завершилась ли транзакция 100 (надо проверить список активных
процессов и их транзакций; такая структура в общей памяти имеет
название ProcArray),
2. а если завершилась, то фиксацией или отменой (свериться с XACT).
Поскольку выполнять проверку по XACT каждый раз накладно,
выясненный однажды статус транзакции записывается в информа-
ционные биты-подсказки заголовка строки. Если один из этих битов
установлен, то состояние транзакции xmin считается известным и
следующей транзакции уже не придется обращаться к XACT.
Почему эти биты не устанавливаются той транзакцией, которая
выполняла вставку? В момент, когда транзакция фиксируется или
отменяется, уже непонятно, какие именно строки в каких именно
страницах транзакция успела поменять. Кроме того, часть этих страниц
может быть вытеснена из буферного кеша на диск; читать их заново,
чтобы изменить биты, означало бы существенно замедлить фиксацию.
Обратная сторона состоит в том, что любая транзакция (даже
выполняющая простое чтение — SELECT) может загрязнить данные
в буферном кеше и породить новые журнальные записи.
11
xmaxxmin данные
100 42,FOO
Удаление
100
committed
aborted
ctid ключ
(0,1) FOO
(0,1)
t
101
выступает
как блокировка
строки
101
При удалении строки в поле xmax текущей версии записывается номер
текущей удаляющей транзакции, а признак оборванной транзакции
сбрасывается. Больше ничего не происходит.
Заметим, что установленное значение xmax, соответствующее
активной транзакции (что определяется другими транзакциями по
ProcArray), выступает в качестве блокировки. Если другая транзакция
намерена обновить или удалить эту строку, она будет вынуждена
дождаться завершения транзакции xmax.
Подробнее блокировки рассматриваются в теме «Блокировки» этого
модуля. Пока отметим только, что число блокировок строк ничем не
ограничено. Они не занимают место в оперативной памяти,
производительность системы не страдает от их количества
(разумеется, за исключением того, что первый процесс, обратившийся
к странице, должен будет проставить биты-подсказки).
12
xmaxxmin данные
Отмена изменений
100 42,FOO
100
committed
aborted
ctid ключ
(0,1) FOO
(0,1)
t
101
101
при первом
обращении к строке
другой транзакцией
t
Отмена изменений работает аналогично фиксации, только в XACT для
транзакции выставляется бит оборванной транзакции. Отмена
выполняется так же быстро, как и фиксация — не требуется выполнять
откат выполненных действий.
Номер прерванной транзакции остается в поле xmax. Когда другая
транзакция обратится к версии строки, она проверит статус транзакции
101 и установит в версию строки признак-подсказку, что транзакция
оборвана. Это будет означать, что на значение в поле xmax смотреть
не нужно.
13
xmaxxmin данные
102 42,BAR0
Обновление
100 42,FOO
100
committed
aborted
ctid ключ
(0,2) BAR
(0,1)
t
101
102
t
(0,1) FOO
(0,2)
102
ссылки
на обе версии
строки
перезаписан
номер отмененной
транзакции
при любом обновлении строки надо изменять все индексы на таблице
в индексе накапливаются ссылки на неактуальные версии
Обновление работает так, как будто сначала выполняется удаление
старой версии строки, а затем — вставка новой.
Старая версия помечается номером текущей транзакции в поле xmax.
Обратите внимание, что новое значение 102 записалось поверх старого
101, так как транзакция 101 была отменена. Кроме того, биты-подсказки
сброшены, так как статус текущей транзакции еще не известен.
В индексной странице появляется второй указатель и вторая строка,
ссылающаяся на вторую версию в табличной странице.
Так же, как и при удалении, значение xmax в первой версии строки
служит признаком того, что строка заблокирована.
Чем плохо такое обновление?
Во-первых, при любом изменении строки приходится обновлять все
индексы, созданные для таблицы (даже если измененные поля не
входят в индекс). Очевидно, это снижает производительность.
Во-вторых, в индексах накапливаются ссылки на исторические версии
строки, которые потом приходится очищать.
Более того, есть особенность реализации B-дерева в PostgreSQL. Если
на индексной странице недостаточно места для вставки новой строки,
страница делится на две и все данные перераспределяются между
ними. Однако при удалении (или очистке) строк две индексные
страницы не «склеиваются» в одну. Из-за этого размер индекса может
не уменьшиться даже при удалении существенной части данных.