27.10.25

Переадресующие и переадресованные записи и размер обратного указателя

Автор: Paul Randal, Forwarding and forwarded records, and the back-pointer size

Этот вопрос всплывает время от времени, в последний раз — сегодня утром во время частного курса (сейчас читает Кимберли): каков размер обратного указателя у переадресованной записи? (И да, я давно не писал ничего по‑настоящему «технического»…)

В куче (heap) возможны переадресующие (forwarding) и переадресованные (forwarded) записи. Это случается, когда запись в куче разрастается так, что уже не помещается на текущей странице. В таком случае запись переносится на новую страницу, а в исходном месте остаётся маленькая переадресующая запись. Переадресующая запись указывает на новое местоположение записи, которая называется переадресованной. Это сделано как оптимизация производительности, чтобы не приходилось изменять все некластерные индексы у кучи из‑за нового адреса записи в этой куче.

К слову: когда запрос использует некластерный индекс для удовлетворения условий, но ему нужны дополнительные столбцы из записи в таблице для формирования списка столбцов в результате, он должен обратиться к фактической записи данных, чтобы достать дополнительные столбцы. Делается это с помощью локатора записи таблицы, который хранится в записи некластерного индекса. Если таблица — куча, локатор записи — это физическое расположение записи данных в куче.

Если таблица — с кластерным индексом (помним, что таблица может быть организована как куча ИЛИ как кластерный индекс, но не одновременно), локатор записи — это набор ключей кластерного индекса записи данных. Оба эти типа локаторов записи гарантированно уникальны. Для локатора записи в куче поиск (часто называемый bookmark lookup) идёт напрямую по физическому адресу записи. Для локатора кластерного индекса поиск использует кластерные ключи, чтобы опуститься по дереву кластерного индекса до уровня листьев.

Если в куче появилась переадресующая запись, когда локатор указывает на это место, Storage Engine приходит туда и «понимает»: ох, записи тут на самом деле нет — она вон там! И ему приходится делать ещё один (потенциально физический) ввод‑вывод, чтобы попасть на страницу с переадресованной записью. Это может сделать кучу менее эффективной по сравнению с эквивалентным кластерным индексом.

О том, что лучше — куча или кластерный индекс, написано много; в общем случае мы рекомендуем кластерный индекс, но бывают особые ситуации, где куча тоже уместна. Эта запись не об этом, и в споры в комментариях я ввязываться не буду. Идите спорить с Кимберли :-)

Вернёмся к сути. Итак, запись переехала на новое место, а на старом оставлена маленькая запись, помогающая при bookmark‑lookup.

Ах да, ещё одно пояснение. Движок хранения при сканировании кучи не будет обрабатывать переадресованную запись, ЕСЛИ он добрался до неё не по цепочке от переадресующей записи. Это предотвращает «гонки», при которых запись могла бы быть обработана дважды (если переадресация произошла во время большого сканирования таблицы).

Хорошо, теперь действительно к сути. Что происходит, если исходная запись снова разрастается и вынуждена переехать ещё раз? Оставит ли она ЕЩЁ одну переадресующую запись при переезде во второе новое место — образуя цепочку переадресующих записей?

Ответ: нет. Исходная переадресующая запись обновляется и начинает указывать на новое расположение переадресованной записи. Это возможно только если переадресованная запись указывает назад на переадресующую — и она действительно указывает.

Вопрос звучит так: каков размер обратного указателя у переадресованной записи? DBCC PAGE его не показывает (простите, переписывая DBCC PAGE для SQL 2005, я забыл это туда добавить). Давайте выведем его опытным путём с помощью скрипта.

Сначала создам базу и таблицу для эксперимента.

CREATE DATABASE DBMaint2008;
GO
USE DBMaint2008;
GO

CREATE TABLE DbccPageTest (intCol1  INT IDENTITY,  intCol2  INT, vcharCol VARCHAR (8000),  lobCol  VARCHAR (MAX));
GO

INSERT INTO DbccPageTest VALUES (1, REPLICATE ('Row1', 600), REPLICATE ('Row1Lobs', 1000));
INSERT INTO DbccPageTest VALUES (2, REPLICATE ('Row2', 600), REPLICATE ('Row2Lobs', 1000));
GO

Используя DBCC IND ('DBMaint2008', 'DbccPageTest', -1), я нахожу, что страница данных имеет идентификатор (1:154). Если снять дамп этой страницы с помощью DBCC PAGE, видно, что обе записи полностью находятся на странице.

Теперь обновлю вторую строку так, чтобы её размер превысил 8000 байт — вынудив её переезд на новую страницу.

UPDATE DbccPageTest
SET vcharCol = REPLICATE ('LongRow2', 1000)
WHERE intCol2 = 2;
GO

И снова посмотрев на страницу (1:154) через DBCC PAGE, вижу, что вторая строка заменена на:

Slot 1 Offset 0x137a Length 9

Record Type = FORWARDING_STUB        Record Attributes = 
Memory Dump @0x66F4D37A

00000000:   049d0000 00010000 00              …………
Forwarding to  =  file 1 page 157 slot 0

Запись заменена переадресующей записью — она указывает на новое расположение на странице (1:157).

DBCC PAGE не выводит обратный указатель в переадресованной записи — как же узнать его размер? Я посмотрю на размер записи, а затем создам кластеризованный индекс на таблице. Это удалит обратный указатель из записи, не меняя остального. Разница в размерах и будет размером обратного указателя.

Сделав DBCC PAGE для страницы (1:157), получаю (фрагмент):

Slot 0 Offset 0x60 Length 8057

Record Type = FORWARDED_RECORD       Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS

Теперь создам кластерный индекс и затем сделаю дам страницы со второй строкой (оставляю как упражнение для читателя :-)):

CREATE CLUSTERED INDEX Dbcc_CL ON DbccPageTest (intCol1);
GO

<figure out which page to look at>

DBCC PAGE ('DBMaint2008', 1, 169, 3);
GO

<partial results>
Slot 0 Offset 0x60 Length 8047

Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS

Разница в размере составляет 10 байт — это и есть размер обратного указателя. Это не совсем очевидно, поэтому вот разъяснение причины изменения размера:

  • Обратный указатель в кучной записи имеет некоторый размер, плюс два байта на смещение в массиве смещений столбцов переменной длины (см. «Как Storage Engine находит столбцы переменной длины?» для подробностей).
  • В кластерном индексе обратного указателя нет вовсе, но это неуникальный кластерный индекс, значит, в каждой записи присутствует пустой столбец‑uniquifier, который занимает как минимум два байта под смещение в массиве смещений столбцов переменной длины.
  • С учётом того, что переход от кучи к неуникальному кластерному индексу дал изменение размера записи на 10 байт при удалении одного «столбца» и добавлении другого, эта 10‑байтная разница и должна приходиться на обратный указатель.

Примечание: если вы создадите уникальный кластерный индекс, увидите изменение размера записи на 12 байт — потому что столбец‑uniquifier не генерируется.

И верно. Вот из чего он состоит:

  • 2 байта — специальный идентификатор столбца (1024) в начале обратного указателя, сигнализирующий, что это именно обратный указатель;
  • 8 байт — адрес записи (2‑байтный идентификатор файла, 4‑байтный номер страницы в файле, 2‑байный номер слота).


Комментариев нет:

Отправить комментарий