Анатомия записи
Пересказ статьи Paul Randal. Inside the Storage Engine: Anatomy of a record
Что представляют собой записи? На упрощенном уровне запись - это физическое хранилище, связанное со строкой в таблице или индексе. Но не все так просто...
Записи данных
- Записи данных хранятся на страницах данных.
- Записи данных хранят строки кучи (heap) или листового уровня кластеризованного индекса.
- Запись данных всегда хранит все колонки строки таблицы - либо по значению, либо по ссылке.
- Если какие-либо столбцы имеют типы данных LOB (text, ntext, image, или новые типы, появившиеся в SQL Server 2005, varchar(max), varbinary(max), XML), то в записи данных хранится указатель, который указывает на текстовую запись на другой странице (корень связанного дерева, хранящего значение LOB). Имеются исключения, когда схема настроена на хранение столбцов LOB в строке (in-row), если это возможно. Т.е. когда значение LOB достаточно мало, чтобы уложиться в ограничения на размер записи данных. Это дает выгоду с точки зрения производительности, поскольку выборка из столбца LOB не потребует дополнительной операции ввода-вывода для считывания текстовой записи.
- В SQL Server 2005 не-LOB столбцы переменной длины (например, varchar, sqlvariant) также могут храниться вне строки (off-row), что позволяет строкам таблицы иметь размер, превышающий 8060 байт (размер дисковой страницы для хранения данных). В этом случае формат хранения аналогичен хранению значений LOB - указатель в записи данных указывает на текстовую запись.
- Есть разница в том, как извлекаются столбцы из кучи и кластеризованных индексов.
- В куче столбцы извлекаются в порядке, заданном оператором CREATE TABLE (разумеется, разделяемых на столбцы постоянной и переменной длины).
- В кластеризованном индексе кластерные ключи становятся первыми физическими столбцами в записи, за которыми в порядке, заданном оператором CREATE TABLE, следуют остальные столбцы.
Перемещаемые/перемещающие записи
- Технически в куче присутствуют только записи данных.
- Перемещаемая запись - это запись данных в куче, которая, будучи обновленной, становится слишком большой, чтобы поместиться на своей исходной таблице, и, таким образом, должна быть перенесена на другую страницу. Эта страница содержит обратный указатель перемещающую запись.
- Перемещающая запись остается на своем месте и указывает на новое положение записи. Её иногда называют перемещающим корешком (forwarding-stub), т.к. все, что она содержит, это местоположение реальной записи данных.
- Это делается для того, чтобы избежать необходимости обновления любых записей некластеризованных индексов, которые содержат обратный указатель непосредственно на оригинальное физическое местоположение записи.
- Оптимизируя обслуживание обновления некластеризованных индексов, этот механизм может вызывать дополнительные операции ввода/вывода при выполнении оператора SELECT. Это происходит потому, что запись некластеризованного индекса указывает на старое положение записи, и может потребоваться дополнительная операция ввода-вывода, чтобы прочитать строку данных из её фактического местоположения. Это аргумент в пользу кластеризованных индексов в рамках дискуссии "куча против кластеризованного индекса".
Индексные записи
- Индексные записи сохраняются на индексных страницах.
- Имеется два типа индексных записей (которые отличаются только тем, какие столбцы они хранят):
- Те, которые хранят строки некластеризованных индексов на листовом уровне некластеризованного индекса.
- Те, которые включают b-tree, составляющие кластеризованные и некластеризованные индексы (т.е. на индексных страницах выше листового уровня кластеризованного или некластеризованного индекса).
- Разницу между ними я более подробно объясню в следующей статье.
- Индексные записи обычно не содержат значений всех столбцов таблицы (хотя некоторые содержат, и они называются покрывающими индексами).
- В SQL Server 2005 записи некластеризованных индексов могут включать значения LOB, как включенные столбцы (при этом детали хранения аналогичны хранению записей данных), и также могут иметь данные превышающие размер записи (опять же как для записей данных).
Текстовые записи
- Текстовые записи хранятся на текстовых страницах.
- Существуют различные типы текстовых записей, которые имеют структуру дерева для сохранения значений LOB на текстовых страницах двух типов. Я объясню как они работают и связаны друг с другом в последующей статье.
- Они также используются для хранения значений столбцов переменной длины, извлеченных из записей данных или индексов для реализации возможности превышения размера строки.
Фантомные записи
- Это записи, которые были удалены со страницы логически, но не физически. Причины такого поведения сложны, но в основном присутствие фантомных записей упрощает блокировки диапазонов ключей и откат транзакции.
- Запись помечается битом, который указывает, что это фантомная запись и не может быть физически удалена до тех пор, пока транзакция, которая привела к появлению фантомной записи, не будет зафиксирована. После фиксации транзакции она удаляется асинхронным фоновым процессом (называемом задачей ghost-cleanup) или преобразуется обратно к реальной записи посредством вставки записи с тем же самым набором ключей.
Записи других типов
- Имеются также записи, которые используют для хранения различные битовые карты распределения, промежуточные результаты операций сортировки, метаданные файлов и баз данных (например, страница заголовков файлов и страница загрузки баз данных). Опять таки, я вернусь к ним в последующих статьях (однако большая очередь выстроилась ).
Структура записи
Все записи имеют одну и ту же структуру вне зависимости от их типа и назначения, однако число и типы столбцов будут различаться. Например, запись данных в таблице со сложной схемой может иметь сотни столбцов различных типов, в то время как запись битовой карты размещения будет иметь единственный столбец, заполняющий всю страницу.
Структура для несжатых записей является следующей:
- Заголовок записи
- Длина 4 байта
- 2 байта метаданных записи (тип записи)
- 2 байта прямого указателя записи на null bitmap (битовая карта NULL-значений).
- Часть записи фиксированной длины, содержащая столбцы, хранящие данные типов фиксированной длины (например, bigint, char(10), datetime)
- Null bitmap
- Два байта для числа столбцов в записи
- Переменное число байт по биту на столбец в записи, независимо от того, допускает этот столбец NULL-значения или нет (это проще по сравнению с SQL Server 2000, который имел по одному биту на только столбец, допускающий NULL).
- Это допускает оптимизацию при чтении столбцов с NULL.
- Массив смещений столбцов переменной длины.
- Два байта на количество столбцов переменной длины
- Два байта на столбец переменной длины, представляющие смещение к концу значения столбца.
- Метка версии
- Только для SQL Server 2005, 14-байтная структура, содержащая временную метку (timestamp) плюс указатель на хранение версии в базе tempdb.
Оптимизация NULL bitmap
Итак, почему null bitmap является оптимизацией?
Во-первых, наличие null bitmap устраняет необходимость хранения специальных NULL-значений для типов данных фиксированной длины. Без null bitmap, как бы вы могли сказать, содержит ли столбец NULL? Для столбцов фиксированной длины вам потребовалось бы определить специальное NULL-значение, которое ограничивает эффективный диапазон сохраняемого типа данных. Для столбцов типа varchar значением может быть пустая строка нулевой длины, поэтому просто проверка длины не работает - вам опять таки потребовалось бы специальное значение. Для всех других типов данных переменной длины вы можете просто проверять длину. Итак, нам нужна битовая карта NULL-значений.
Во-вторых, она сокращает число циклов процессора. Если нет null bitmap, тогда требуются лишние команды, выполняемые для столбцов фиксированной и переменной длины.
Для фиксированной длины:
1. Чтение значения в сохраненном столбце (возможно неудачное обращение в кэш процессора).
2. Загрузка предопределенного для данного типа данных NULL-значения (возможно неудачное обращение в кэш процессора, но только для первого чтения в случае выборки множества строк).
3. Выполнение сравнения этих двух значений.
Для переменной длины:
1. Вычисление смещения массива переменных длин.
2. Чтение числа столбцов переменной длины (возможно неудачное обращение в кэш процессора).
3. Вычисление позиции в массиве смещений переменной длины для чтения.
4. Чтение смещения столбца оттуда (возможно неудачное обращение в кэш процессора).
5. Чтение следующего смещения (возможно неудачное обращение в кэш процессора, если смещение на 4 шаге попало на границу размера строки кэша).
6. Сравнение этих смещений для проверки на совпадение.
При использовании null bitmap все, что вам нужно, это:
1. Прочитать смещение null bitmap (возможно неудачное обращение в кэш процессора).
2. Вычислить дополнительное смещение бита NULL, который вы хотите прочитать.
3. Прочитать его (возможно неудачное обращение в кэш процессора).
Все это относится к поиску единственного столбца фиксированной длины, но для столбцов переменной длины и для выбора множества строк очевидно преимущество использования битовой карты NULL.
Использование DBCC IND и DBCC PAGE для исследования строки
Давайте создадим тестовую таблицу:
USE [master];
GO
IF DATABASEPROPERTY (N'recordanatomy', 'Version') > 0 DROP DATABASE [RecordAnatomy];
GO
CREATE DATABASE [RecordAnatomy];
GO
USE [RecordAnatomy];
GO
CREATE TABLE [example] ([destination] VARCHAR(100), [activity] VARCHAR(100), [duration] INT);
GO
INSERT INTO [example] VALUES ('Banff', 'sightseeing', 5);
INSERT INTO [example] VALUES ('Chicago', 'sailing', 4);
GO
И опять мы можем использовать DBCC IND для для нахождения страницы:
DBCC IND ('recordanatomy', 'example', 1);
GO
Напомню, что нам потребуется установить trace-flag, чтобы вывод DBCC PAGE шел в консоль, а не в журнал ошибок. Вывод будет содержать что-то подобное следующему:
Slot 0 Offset 0x60 Length 33
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Memory Dump @0x5C76C060
00000000: 30000800 05000000 0300f802 00160021 †0..............!
00000010: 0042616e 66667369 67687473 6565696e †.Banffsightseein
00000020: 67†††††††††††††††††††††††††††††††††††g
Slot 0 Column 0 Offset 0x11 Length 5
destination = Banff
Slot 0 Column 1 Offset 0x16 Length 11
activity = sightseeing
Slot 0 Column 2 Offset 0x4 Length 4
duration = 5
Давайте использовать структуру записи, которую я описал выше, и пройдем по этой записи, чтобы увидеть, как все хранится.
- Байт 0 представляет собой байт TagA метаданных записи.
- Это 0х30, что соответствует 0х10 (бит 4) и 0х20 (бит 5). Бит 4 означает, что запись имеет null bitmap, а бит 5 - что запись имеет столбцы переменной длины. Если бы было также установлено 0х40 (бит 6), это означало бы, что запись имеет метку версии. Установка 0х80 (бит 7) означало бы, что байт 1 имеет в ней значение.
- Биты 1-3 байта 0 указывают тип записи . Вот возможные варианты:
- 0 = первичная запись. Запись данных в куче, которая не переносилась, или запись данных на листовом уровне кластерного индекса.
- 1 = перенесенная запись
- 2 = переносящая запись
- 3 = Индексная запись
- 4 = фрагмент blob
- 5 = фантомная индексная запись
- 6 = фантомная запись данных
- 7 = фантомная запись версии. Специальная 15-байтовая запись, содержащая однобайтовый заголовок записи плюс 14-байтовую метку версии, которая используется при определенных обстоятельствах.
- В нашем примере ни один из этих битов не установлен, поэтому наша запись является первичной (primary) записью. Если запись является индексной, то байт 0 имел бы значение 0х36. Напомню, что тип записи начинается с бита 1, а не 0, поэтому для получения значения типа записи в байте нужно значение, указанное выше, сдвинуть влево на 1 бит (умножение на 2).
- Байт 1 - это байт TagB метаданных записи. Он может быть либо 0х00, либо 0х01. Если 0х01, это означает запись типа фантомная перенесенная. В этом случае 0х00 - это значение, которое мы ожидали получить в байте TagA.
- Байты 2 и 3 представляют собой смещение null bitmap в записи. Это 0х0008 (многобайтные значения сохраняются, начиная с младшего байта). Это означает, что имеется 4-х байтная часть записи фиксированной длины, начинающаяся с байта 4. Мы ожидали этого, поскольку нам известна схема таблицы.
- Байты 4 - 7 есть порции фиксированной длины. Опять таки, поскольку мы знаем схему таблицы, то интерпретируем эти байты как 4-байтное целое. Без этих знаний вам придется угадывать. Таким образом 0x00000005 - это ожидаемое значение столбца duration.
- Байты 8 и 9 - это количество столбцов в записи. Мы имеем 0х0003, что правильно. При условии, что имеется только 3 столбца, null bitmap при одном бите на столбец поместится в один байт.
- Байт 10 есть null bitmap. Его значение 0xF8. Нам потребуется преобразовать это значение в двоичную форму, чтобы понять его смысл: 0xF8 = 11111000. Смысл таков - биты 0-2 представляют столбцы 1-3, и они все равны 0, что означает отсутствие NULL. Биты 3-7 пресдтавляют несуществующие столбцы, и они установлены в 1 для ясности.
- Байты 11 и 12 есть количество столбцов переменной длины в записи. Данное значение равно 0х0002, что, как мы знаем, верно. Это означает, что будет два двухбайтовых входа в массиве смещений столбцов переменной длины. Это будут байты 13-14 и 15-16, содержащие значения 0х0016 и 0х0021 соответственно. Вспомним, что входы в массиве смещений столбцов переменной длины указывают на конец значения столбца - это делается для того, чтобы знать длину каждого столбца без необходимости дополнительного хранения их длин.
- Итак, последнее смещение есть байты 15 и 16, т.е. начинаться первый столбец переменной длины должен с байта 17 (или 0х11 в шестнадцатеричной кодировке), что согласуется с дампом DBCC PAGE. Смещением конца первого столбца переменной длины является 0х0016, поэтому первое значение занимает байты с 17 по 21 включительно. Само значение равно 0х42616E6666. Мы знаем из метаданных таблицы, что первым является столбец destination (типа varchar). Преобразование в ASCII дает нам значение столбца - 'Banff'. Используя аналогичную логику, выясняем, что второе значение занимает байты 22-32 включительно и равно 'sightseeing'. Оба эти значения совпадают с нашими ожиданиями.
Обратные ссылки
Автор не разрешил комментировать эту запись
Комментарии
Показывать комментарии Как список | Древовидной структурой