13.10.25

Как Storage Engine находит столбцы переменной длины?

Автор: Paul Randal, Search Engine Q&A #27: How does the storage engine find variable-length columns?

Этот вопрос прозвучал на одном из моих недавних занятий: если массив смещений столбцов переменной длины в записи хранит только смещения, то как механизм хранения находит конкретный столбец переменной длины?

Сомнение возникает потому, что в самой записи нет пометки, какой столбец переменной длины где расположен — как это работает? Ответ состоит из двух частей: это и NULL‑битовая карта (null bitmap) в записи, и метаданные таблицы/индекса, хранящиеся в системных таблицах. Все столбцы переменной длины имеют фиксированную «позицию» внутри переменной части записи в том случае, если их значение не равно NULL. Давайте покажу, что это значит.

Для начала создадим тестовую таблицу с одной записью, где все столбцы переменной длины равны NULL, и посмотрим на запись через DBCC PAGE:

CREATE TABLE vartest (c1 INT, c2 VARCHAR (100), c3 VARCHAR (100), c4 varchar (100));
GO
INSERT INTO vartest VALUES (1, NULL, NULL, NULL);
GO

DBCC TRACEON (3604);
DBCC PAGE (test, 1, 152, 1);
GO

Slot 0, Offset 0x60, Length 11, DumpStyle BYTE
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x66F4C060

00000000: 10000800 01000000 0400fe…………..

Выделенная часть — 0x0400fe — это NULL‑битовая карта. Число 0x0400 читается наоборот как 0x0004, что означает количество столбцов в записи. Значение 0xfe — сама битовая маска: в двоичном виде 11111110. Значит, все столбцы записи, кроме первого, равны NULL (хотя NULL‑битовая карта обязана хранить биты только для 4 столбцов, все неиспользуемые биты в карте также установлены в «NULL»). Можно заметить, что массива смещений столбцов переменной длины нет — потому что все столбцы переменной длины — NULL.

Теперь сделаем средний столбец переменной длины ненулевым и снова посмотрим запись через DBCC PAGE:

UPDATE vartest SET c3 = 'c3c3c3c3';
GO
DBCC PAGE (test, 1, 152, 1);
GO

Slot 0, Offset 0x60, Length 25, DumpStyle BYTE
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Memory Dump @0x67FEC060

00000000: 30000800 01000000 0400fa02 00110019 0……………
00000010: 00633363 33633363 33……………..c3c3c3c3

Видим, что нуль‑битовая карта (выделенная ранее часть) изменилась на 0xfa (в двоичном виде 11111010) — это отражает, что первый и третий столбцы стали ненулевыми. Как только хотя бы один столбец переменной длины в записи — не NULL, массив смещений столбцов переменной длины заполняется для всех столбцов переменной длины вплоть до последнего ненулевого столбца включительно. У массива есть счётчик записей (это 0200, которое читается как 0x0002), а затем для каждого столбца хранится смещение к началу следующего столбца, чтобы не хранить длину. Разность между соседними смещениями начала столбцов и есть длина, причём первая длина вычисляется, считая началом первого столбца конец самого массива. В приведённой записи массив содержит два значения, указывающие на окончания по смещениям 0x0011-1 и 0x0019-1 в записи. Сам массив заканчивается на 0x0010, так что первая запись массива фактически указывает на «пустое» значение — при этом благодаря нуль‑битовой карте мы понимаем, что это именно NULL, а не ненулевое пустое значение.

Теперь сделаем ненулевым первый столбец переменной длины и посмотрим, что произошло с записью:

UPDATE vartest SET c2 = 'c2c2c2c2';
DBCC PAGE (test, 1, 152, 1);
GO

Slot 0, Offset 0x60, Length 33, DumpStyle BYTE
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Memory Dump @0x6714C060

00000000: 30000800 01000000 0400f802 00190021 0…………..!
00000010: 00633263 32633263 32633363 33633363 .c2c2c2c2c3c3c3c
00000020: 33………………………………3

Нуль‑битовая карта изменилась с 0xfa на 0xf8, отражая новый ненулевой столбец. Переменная часть записи была переупорядочена так, чтобы новый ненулевой столбец занял «своё» место (в шестнадцатеричном дампе видно, что последовательность c2c2c2c2 теперь идёт перед c3c3c3c3), а смещения обновлены соответствующим образом. Массив по‑прежнему содержит только два значения. Теперь зададим ненулевое значение для последнего столбца:

UPDATE vartest SET c4 = 'c4c4c4c4';
DBCC PAGE (test, 1, 152, 1);
GO

Slot 0, Offset 0x81, Length 43, DumpStyle BYTE
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Memory Dump @0x66FCC081

00000000: 30000800 01000000 0400f003 001b0023 0…………..#
00000010: 002b0063 32633263 32633263 33633363 .+.c2c2c2c2c3c3c
00000020: 33633363 34633463 346334……………3c3c4c4c4c4

Нуль‑битовая карта теперь показывает, что все 4 столбца — ненулевые. Счётчик элементов массива смещений столбцов переменной длины вырос до 3, и новая запись добавлена в конец (так как это последний столбец переменной длины).

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

Надеюсь, теперь всё стало понятно!

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

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