Многоверсионность
HOT-обновления
16
Авторские права
© Postgres Professional, 2016–2025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
HOT-обновления
Внутристраничная очистка
3
Обычное обновление
xmin committed
xmin aborted
xmax committed
xmax aborted
t
xmin данные
100 t 42,FOO
ctid ключ
(0,1)
указатель
normal
(0,2) BAR
normal(0,2)
(0,3) BAZ
t101 t 42,BAR102
101
xmax
ссылки
на все версии
строки
heap hot upd
heap only tuple
индекс
по столбцу,
данные в котором
меняются
normal(0,3) t102 42,BAZ
(0,1) FOO
Напомним, что при обычном обновлении в индексе создаются ссылки
на все версии строки, присутствующие в табличных страницах.
(Исключение составляет индекс BRIN, который не содержит ссылок
на отдельные табличные строки; подробности устройства индексов
различных типов рассматриваются в курсе QPT «Оптимизация
запросов»).
4
Проблемы обновления
При любом обновлении строки надо изменять индекс
страдает производительность вставок и изменений
В индексе накапливаются ссылки на неактуальные версии
размер индекса растет, требуется очистка
Все сложности умножаются на количество индексов,
построенных на таблице
Чем это плохо?
Во-первых, при любом изменении строки придется обновлять все
индексы, созданные для таблицы (даже если измененные поля не
входят в индекс). Очевидно, это снижает производительность.
Во-вторых, в индексах накапливаются ссылки на исторические версии
строк, которые потом приходится очищать.
Естественно, чем больше индексов создано на таблице, тем
с большими сложностями приходится сталкиваться.
Более того, есть особенность реализации B-дерева в PostgreSQL.
Если на индексной странице недостаточно места для вставки новой
записи, страница делится на две и все данные перераспределяются
между ними. Однако при удалении (или очистке) записей две
индексные страницы не «склеиваются обратно» в одну. Из-за этого
размер индекса может не уменьшиться даже при удалении
существенной части данных. В PostgreSQL 14 была добавлена
оптимизация «восходящее удаление кортежей» (bottom-up deletion),
позволяющая перед добавлением индексной записи удалить из
страницы уже ненужные. Ее действие касается индексов, которые
не были логически изменены операторами UPDATE; в ряде случаев
это позволяет избежать деления индексной страницы.
5
HOT-обновление
xmin committed
xmin aborted
xmax committed
xmax aborted
heap hot upd
heap only tuple
t
ctidxmin данные
100 t 42,FOO
ctid ключ
(0,1)
указатель
normal
(0,1) 42
normal(0,2) t101 t 42,BAR102 (0,3)
(0,2)101
xmax
ссылка
только на первую
версию строки
normal(0,3) t102 42,BAZ(0,3)t
tt
t
индекс
по столбцу,
данные в котором
не меняются
Однако если индекс создан по полю, значение которого не изменилось
в результате обновления строки, то нет смысла создавать
дополнительную запись в B-дереве, содержащую то же самое значение
ключа. Именно так работает оптимизация, называемая HOT-
обновлением — Heap-Only Tuple Update.
При таком обновлении в индексной странице находится лишь одна
запись, ссылающаяся на первую версию строки табличной страницы.
А внутри табличной страницы организуется цепочка версий:
строки, которые изменены и входят в цепочку, маркируются битом
Heap Hot Updated;
строки цепочки, на которые нет ссылок из индекса, маркируются
битом Heap Only Tuple (то есть — «только табличная версия
строки»);
версии строк связаны в список с помощью поля ctid, входящего
в заголовок версий.
Если при сканировании индекса PostgreSQL попадает в табличную
страницу и обнаруживает версию, помеченную как Heap Hot Updated,
он понимает, что надо пройти дальше по цепочке обновлений.
азумеется, для всех полученных таким образом версий строк
проверяется видимость, прежде чем они будут возвращены клиенту.)
6
HOT-обновление
Значения индексированных столбцов не должны изменяться
иначе придется добавить индексную запись, ссылающуюся на новую
версию строки, и версию нельзя будет пометить как «heap only»
Цепочка обновлений — только в пределах одной страницы
не требуется обращение к другим страницам,
обход цепочки не ухудшает производительность
если в табличной странице не хватает места для новой версии,
цепочка обрывается (как если бы оптимизация не работала)
место в странице можно зарезервировать, уменьшив параметр
хранения таблицы fillfactor (100 % → 10 %)
Подчеркнем, что HOT-обновления работают только в случае, если
не изменяется ни один ключ в индексах. Иначе в каком-либо индексе
появилась бы ссылка непосредственно на новую версию строки, что
противоречит идее этой оптимизации. Только упомянутый выше индекс
BRIN не препятствует HOT-обновлению (в нем нет ссылок на
табличные строки).
В том числе HOT-обновления применяются и к таблицам, на которых
нет вообще ни одного индекса: при обновлении любых полей такой
таблицы будет строиться цепочка версий.
Оптимизация действует только в пределах одной страницы, поэтому
дополнительный обход цепочки не требует обращения к другим
страницам и не ухудшает производительность.
Однако если на странице не хватит свободного места, чтобы
разместить новую версию строки, цепочка прервется. На версию
строки, размещенную на другой странице, придется сделать и ссылку
из индекса.
Поэтому при частых обновлениях неиндексированных полей может
иметь смысл уменьшить параметр хранения fillfactor. Этот параметр
определяет пороговый процент занятого на странице места, после
которого вставка новых строк в эту страницу будет запрещена.
Значение по умолчанию – 100%, можно уменьшать до 10%.
Оставшееся место резервируется для обновлений: в этом случае новая
версия строки может занять свободное место на той же странице.
(С другой стороны, чем выше fillfactor, тем компактнее располагаются
записи и, соответственно, размер таблицы получается меньше.)
8
Внутристраничная очистка
Выполняется при любом обращении к странице
если ранее выполненное обновление не нашло места
для новой версии строки на этой же странице
если страница заполнена более чем на fillfactor или более чем на 90%
Действует в пределах одной табличной страницы
не освобождает указатели, на которые могут ссылаться индексы
не обновляет карту свободного пространства
не обновляет карту видимости
При обращении к странице — как при обновлении, так и при чтении —
может происходить быстрая внутристраничная очистка, если
PostgreSQL сочтет, что место на странице заканчивается. Должно
выполниться одно из условий:
1. Ранее выполненное на этой странице обновление не обнаружило
достаточного места, чтобы разместить здесь же новую версию
строки и установило в заголовке страницы соответствующий признак,
по которому при следующем обращении к ней срабатывает очистка.
2. Страница заполнена более чем на fillfactor или более чем на 90%
очистка сработает сразу.
Внутристраничная очистка убирает версии строк, не видимые
ни в одном снимке (находящиеся за горизонтом базы данных),
но работает строго в пределах одной табличной страницы. Указатели
на версии строк не освобождаются, так как на них могут быть ссылки
из индексов, а это уже другая страница — она не очищается.
Карта свободного пространства не обновляется из экономии ресурсов,
а также из соображения, что освобожденное место лучше приберечь
для обновлений, а не для вставок. Также не обновляется и карта
видимости.
Тот факт, что страница может очищаться при чтении, означает, что
запрос SELECT может вызвать изменение страниц. Это еще один такой
случай, в дополнение к рассмотренному в теме «Страницы и версии
строк» изменению битов-подсказок.
9
До HOTчистки
xmin committed
xmin aborted
xmax committed
xmax aborted
heap hot upd
heap only tuple
ctidxmin данные
ctid ключ
(0,1)
указатель
normal
(0,1) 42
normal(0,2)
xmax
normal(0,3)
t100 t 42,FOO(0,2)101
t101 t 42,BAR102 (0,3)
t102 42,BAZ(0,3)
t
t t
t
Частный, но важный случай внутристраничной очистки представляет
собой очистка при HOT-обновлениях.
На рисунке приведена ситуация до очистки. В таблице три версии
одной и той же строки. На первую из них (0,1) ссылается индекс,
остальные две (0,2) и (0,3) помечены как «только табличные».
Версии строк (0,1) и (0,2) неактуальны, не видны ни в одном снимке
и могут быть удалены.
10
После HOTчистки
xmin committed
xmin aborted
xmax committed
xmax aborted
heap hot upd
heap only tuple
ctidxmin данные
ctid ключ
(0,1)
указатель
(0,1) 42
(0,2)
xmax
normal(0,3) t102 42,BAZ(0,3)t
unused
redirect
ссылка
остается
активной
указатель
освобожден
После срабатывания внутристраничной очистки неактуальные версии
строк удаляются.
При HOT-обновлениях в индексе может быть только одна ссылка на
версию строки, представляющую собой «голову» HOT-цепочки, которая
поддерживается внутри одной табличной страницы. При любых
изменениях версий строки указатель должен оставаться на своем
месте и ссылаться на голову цепочки.
Поэтому применяется двойная адресация: для указателя, на который
ссылается индекс — в данном случае (0,1), — используется статус
«redirect», перенаправляющий на нужную версию строки.
Указатель на вторую версию (0,2) больше не нужен. Он получает статус
«unused» и будет использован при вставке какой-нибудь новой версии
строки.
Все оставшиеся версии строк сдвигаются вместе так, чтобы свободное
место на странице было представлено одним фрагментом.
Соответствующим образом изменяются и значения указателей.
Благодаря этому не возникает проблем с фрагментацией свободного
места в странице.
12
До очистки
xmin committed
xmin aborted
xmax committed
xmax aborted
xmin данные
ctid ключ
(0,1)
указатель
normal
(0,2) BAR
normal(0,2)
(0,3) BAZ
xmax
heap hot upd
heap only tuple
normal(0,3)
(0,1) FOO
t t 42,FOO
t t 42,BAR
t 42,BAZ
100
101
102
101
102
Разумеется, внутристраничная очистка работает и для версий,
появившихся в результате обычного (не HOT) обновления.
На рисунке приведена ситуация до очистки. В табличной странице три
версии одной строки. Две из них неактуальны, не видны ни в одном
снимке и могут быть удалены.
В индексе есть три ссылки на каждую из версий строки.
13
normal
После очистки
xmin committed
xmin aborted
xmax committed
xmax aborted
xmin данные
ctid ключ
(0,1)
указатель
(0,2) BAR
(0,2)
(0,3) BAZ
xmax
heap hot upd
heap only tuple
dead
индекс
не очищается
(0,3) t102 42,BAZ
(0,1) FOO
dead
указатели
помечены, но не
освобождены
Если выполнено условие для срабатывания внутристраничной очистки,
две неактуальные версии строки (0,1) и (0,2) могут быть ею удалены.
Тогда указатели на удаленные версии получают статус «dead»,
а освободившееся место может быть использовано для вставки новых
версий.
В отличие от ситуации с HOT-обновлениями, здесь нельзя освободить
указатели на удаленные версии строк, поскольку на них существуют
ссылки из индексной страницы. При индексном доступе PostgreSQL
может получить (0,1) или (0,2) в качестве идентификатора версии
строки, а затем попробовать пройти по этому идентификатору
в табличную страницу, но благодаря статусу указателя обнаружит,
что эта версия уже не существует.
(На рисунке не показаны указатели на индексные строки. На самом
деле, в первый раз обнаружив отсутствие версии табличной строки,
PostgreSQL изменит и статус указателя в индексной странице, чтобы
повторно не обращаться к табличной странице.)
Принципиально то, что внутристраничная очистка работает только
в пределах одной табличной страницы и не очищает страницы индекса.
14
Итоги
Если изменяемый столбец не входит ни в один индекс
и на странице есть место — применяется HOT-обновление
При удобном случае автоматически выполняется быстрая
внутристраничная очистка
При частых обновлениях можно подумать об уменьшении
fillfactor
15
Практика
1. Воспроизведите ситуацию внутристраничной очистки
без участия HOT-обновлений.
Проверяйте содержимое табличной и индексной страниц
с помощью расширения pageinspect.
2. Воспроизведите ситуацию HOT-обновления на таблице
с индексом по некоторым полям.
3. Воспроизведите ситуацию HOT-обновления, при которой
внутристраничная очистка не освобождает достаточно места
на странице и новая версия создается на другой странице.
Сколько строк будет в индексе в этом случае?
1. Запрос для индексной страницы появлялся в демонстрации к теме
«Страницы и версии строк» этого модуля:
SELECT itemoffset, ctid
FROM bt_page_items('имя-индекса',1);