По материалам статьи Paul Randal:
Poking about with DBCC PAGE (Part 1 of ?)
Перевод Ирины Наумовой
На свою статью How to use DBCC PAGE
я получил множество откликов с просьбой рассказать побольше об использовании DBCC
PAGE. Я планировал подождать с этим и сначала написать больше про DBCC CHECKDB,
потом перейти к разным примерам ошибок, и только потом снова начать писать про DBCC
PAGE. Однако, друг сделал мне предложение, от которого я не смог отказаться, так что я
начинаю эту серию статей сейчас.
Начнём с того, как заполучить запись напрямую. Да, я знаю, что опция 3 команды DBCC
PAGE позволяет это сделать - но как это происходит? Иногда невозможно получить
метаданные таблицы, что делает невозможным использование DBCC PAGE с опцией 3. В
таких случаях Вам нужно будет сделать это самим. К тому же имеется множество любопытных
людей, которые любят покопаться в битах и байтах. Для них получать данные вручную -
это забава (если они увлечены этим также как я).
Для экспериментов я создал на SQL Server 2005 базу данных dbccpagetest, которую я
заархивировал и присоединил к статье. Таким образом, Вы можете использовать те же
самые данные и страницы что и я и не заниматься скучной работой со страницами,
используемыми на PFS страницах (то, что я объяснял в статье How to use DBCC PAGE).
Вы не сможете присоединить эту базу к SQL Server 2000, но всегда можно скачать SQL
Server 2005 Express Edition и проводить эксперименты на нем. Разархивируйте файлы
и присоедините их, используя инструкцию CREATE DATABASE ... FOR ATTACH:
CREATE DATABASE dbccpagetest
ON (FILENAME='C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\dbccpagetest.mdf')
FOR ATTACH;
GO
|
Обратите внимание, что, если не менять имена файлов во время создания, Вам понадобится
лишь определить первичный файл в инструкции CREATE DATABASE. Это здорово.
В базе данных имеется одна таблица, в которой две строки, она создана с помощью
следующего кода на T-SQL:
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
|
Страница данных - это страница (1:152). Получая дамп этой страницы при помощи опции 3,
мы видим результат, содержащий следующую информацию:
Slot 0 Offset 0x60 Length 33
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Memory Dump @0x44DFC060
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
|
Ранее я уже писал о структуре записей, сейчас повторюсь:
Заголовок записи
Длина заголовка - 4 байта;
Два байта метаданных записи (тип записи);
Два байта указывают вначале записи на битовую маску NULL.
Фиксированная часть длины записи - содержит хранимые типы столбцов данных,
которые имеют фиксированную длину (например: bigint, char (10), datetime).
Битовая маска NULL
Два байта для счетчика столбцов записи;
Переменное число байт для хранения одного бита на каждый столбец записи,
независимо от того, допускаются ли для этого столбца NULL-значения (это отличается
от того, что было в SQL Server 2000, в котором отводилось по одному биту для nullable
столбцов) и является более простым решением;
Эта маска позволяет оптимизировать чтение столбцов, которые имеют NULL-значения.
Массив смещения для столбцов переменной длины
Два байта счётчика столбцов переменной длины;
Два байта на каждый столбец переменной длины, задающие смещение на начало значения столбца.
Тэг версионности
Появился в SQL Server 2005;
Является 14-байтной структурой, которая содержит временную метку и указатель
в хранилище версий в tempdb.
Итак, давайте посмотрим на данные из фрагмента дампа и выясним, что все это означает.
Я не собираюсь вдаваться во все подробности относительно каждого возможного значения
каждого байта, но дам достаточно информации, чтобы Вы могли самостоятельно расшифровать
полученные данные.
Байт 0 - это байт TagA записи метаданных. 0х30 соответствует 0х10 (бит 4) и 0х20 (бит 5).
Бит 4 означает, что запись имеет битовую маску NULL, а бит 5, что запись имеет поля
переменной длины. Если установлено также 0х40 (бит 6), это означает что запись
имеет тег версионности. Если установлено 0х80 (бит 7), то первый байт содержит в себе значение.
Биты 1-3 байта 0 содержат тип записи. Они могут принимать следующие значения:
0 = primary record (первичная запись)
1 = forwarded record (перемещенная запись)
Запись данных в куче, которая была модифицирована, не поместилась
на странице, где она размещалась изначально, и поэтому была перемещена на другую страницу.
Перемещаемая запись меняет свое местоположение и вместо неё остаётся указатель на
новое положение записи. Это сделано для того, что бы не вносить изменения в записи
некластеризованных индексов, которые указывают непосредственно на первоначальное
физическое местоположение записи.
2 = forwarding record (перемещаемая запись)
3 = index record (индексные записи)
4 = фрагмент BLOB'а
5 = фантомные индексные записи
6 = фантомные записи данных
7 = фантомные записи версионности
Специальные 15 - байтовые записи, в которых один байт занимает
заголовок, а 14 байт содержат тег версионности. Используются при некоторых обстоятельствах
(например, при фантомности версионных blob записей).
В нашем примере, ни один из этих битов не установлен, что означает, что мы имеем дело
с первичной записью. Если бы запись была индексной, байт 0 имел бы значение 0x36.
Помните, что тип записи начинается с бита 1, а не с бита 0, поэтому значение типа
записи, приведенное выше нужно сдвинуть влево на 1 бит (умножить на два) чтобы
получить его значение в байте.
Байт 1 это байт TagB записи метаданных. Он может быть 0х00 или 0х01. Если он 0х01,
это означает что тип записи - фантомная перемещенная запись. В нашем случае он
0х00, что и можно было предположить по значению TagA.
Байты 2 и 3 - это смещение битовой маски NULL в записи. Оно равно 0х0008 (DBCC
PAGE представляет много-байтовые значения в шестнадцатеричном виде, где вначале
идет первый байт). Это означает, что имеется 4-байтовая фиксированная часть длины
записи, начинающейся с байта 4. Мы ожидали этого, поскольку знаем схему таблицы.
Байты с 4 по 7 - фиксированная часть длины. И снова, поскольку мы знаем схему таблицы,
мы знаем, что они интерпретируются как 4-байтовое целое (integer). Если бы мы не знали
этого, то могли бы предположить. Поэтому значение - 0x00000005- это то, что мы ожидали
увидеть в качестве значения столбца duration.
Байты 8 и 9 содержат количество полей в записи. Наблюдаемое значение 0x0003 несомненно
является правильным. Учитывая, что имеются только 3 столбца, битовая маска NULL, каждый
бит которой соответствует одному из столбцов, будет занимать один байт.
Байт 10 - это битовая маска NULL. У нас это значение 0xF8 - откуда оно взялось?
Преобразуйте его к двоичному виду, что получится? 11111000. Все становится понятно -
биты 0-2 представляют столбцы 1-3, и они - все 0, это означает что столбцы не NULL.
Биты 3-7 представляют несуществующие столбцы, и они установлены в 1 (для ясности).
Так что значение 0xF8 имеет смысл.
Байты 11 и 12 содержат количество в записи столбцов переменной длины. Это значение -
0x0002, которое как можно догадаться, является правильным. Это означает, что имеются
два двухбайтовых элемента в массиве смещений столбцов переменной длины. Это будут
байты 13-14 и 15-16, которым соответствуют значения 0x0016 и 0x0021 соответственно.
Поясню, проще говоря, массив смещений столбцов переменной длины сохраняет смещения
на начало значения столбца. Фактически, элементы массива указывают на начало следующего
значения столбца - это сделано для того, чтобы мы знали длину каждого столбца (т.к.
они переменной длины). Обратите внимание, что смещение первого значения столбца
переменной длины не сохранено - этого не требуется, потому что по определению оно
должно начинаться справа после последнего смещения в массиве смещений столбцов
переменной длины.
Итак, последнее смещение - байты 15 и 16, означают, что смещение начала первого
столбца переменной длины должно быть 17 байт (или 0x11 в шестнадцатеричном виде),
что соответствует результату, полученному с помощью DBCC PAGE. Смещение второго
столбца переменной длины - 0x0016, так что первое значение - от байта 17 до байта
21 включительно. Это значение - 0x42616E6666. Мы знаем из метаданных таблицы, что
это - первый столбец типа varchar - destination. Воспользовавшись вполне удобной
таблицей ASCII преобразований
http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters мы легко вычисляем,
что это транслируется как 'Banff'.
Используя такую же логику, второе значение - это байты с 22 по 32 включительно, имеет
значение 'sightseeing'. Оба этих значения мы и ожидали увидеть.
Вот и все. Мы только что расшифровали всю запись. Теперь попробуйте сделать это со
второй строкой таблицы.
[В начало]