Skip to content

Анатомия записи

Пересказ статьи 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. Это происходит потому, что запись некластеризованного индекса указывает на старое положение записи, и может потребоваться дополнительная операция ввода-вывода, чтобы прочитать строку данных из её фактического местоположения. Это аргумент в пользу кластеризованных индексов в рамках дискуссии "куча против кластеризованного индекса".


Индексные записи



  • Индексные записи сохраняются на индексных страницах.

  • Имеется два типа индексных записей (которые отличаются только тем, какие столбцы они хранят):

    1. Те, которые хранят строки некластеризованных индексов на листовом уровне некластеризованного индекса.

    2. Те, которые включают 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'. Оба эти значения совпадают с нашими ожиданиями.

Категории: T-SQL

Обратные ссылки

Нет обратных ссылок

Комментарии

Показывать комментарии Как список | Древовидной структурой

Нет комментариев.

Автор не разрешил комментировать эту запись

Добавить комментарий

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Добавленные комментарии должны будут пройти модерацию прежде, чем будут показаны.