Header RSS-подписка на обновления сайта eMail-подписка на обновления сайта

К вопросу уникальности индексов. Часть 3/4.

Уникальный кластерный + не-уникальный не-кластерный.


366bef3a

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
USE master
go
CREATE DATABASE [~DB~]
ON PRIMARY (
    NAME = '~DB~_Data',
    FILENAME = 'c:\sqlCMD.ru\~DB~.mdf',
    SIZE = 20 MB )
LOG ON (
    NAME = '~DB~_Log',
    FILENAME = 'c:\sqlCMD.ru\~DB~.ldf',
    SIZE = 20 MB )
GO
USE [~DB~]
GO
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'T1') DROP TABLE T1
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Tsup') DROP TABLE Tsup
CREATE TABLE Tsup (PageFID  tinyint, PagePID int, IAMFID   tinyint, IAMPID  int, ObjectID  int, IndexID  tinyint, PartitionNumber tinyint,
 PartitionID bigint, iam_chain_type  varchar(30), PageType  tinyint, IndexLevel  tinyint, NextPageFID  tinyint, NextPagePID  int, PrevPageFID  tinyint, PrevPagePID int)
CREATE TABLE T1 (col1 int, col2 int)
GO
CREATE UNIQUE CLUSTERED INDEX CL_uq_Index ON T1(col1)
GO
CREATE  NONCLUSTERED INDEX NONCL_uq_Index ON T1(col2)
GO
declare @i int, @j int
SET @i = 0
SET @j = 0
BEGIN TRANSACTION
    WHILE @i < 250000 --loop1 counter
    BEGIN
        WHILE @j < 10 --loop2 counter
        BEGIN
            insert into T1 VALUES (10000+@i+@j, 500000+@i)
            set @j += 1
        END
        set @i += 10
        set @j = 0
    END
COMMIT TRANSACTION
INSERT INTO Tsup EXEC ('DBCC IND ([~DB~], T1, -1)');
SELECT PagePID,IndexID,PageType,IndexLevel FROM Tsup WHERE IndexLevel>0

Как видите, мы вносим десять повторяющихся значений для col2 и лишь на одиннадцатом переходим к значению следующему («перепрыгнув» при этом через десяток). Каждое же значение первой колонки, разумеется остается строго уникальным, иначе «заругается» кластерный индекс. Итого, общая схема записей получается «10 различных значений col1»=«10 одинаковых значений col2». При этом, будьте внимательны, общее число строк вставляемых в таблицу T1 не меняется, и остается таким же как оно было в первом опыте — 250 тыс. То есть мы вправе ожидать примерно равного расхода ресурсов на поддержку такой комбинации обоих индексов (как выяснится по ходу данного раздела эти ожидания не имеют ничего общего с реальностью). Ну а резалт-сет нашего тест-скрипта будет таким:

PagePID IndexID PageType    IndexLevel
94      1       2           1
1099    1       2           2
1100    1       2           1
110     2       2           1
1061    2       2           2
1062    2       2           1

Если соотнести резалт-сет этот и аналогичный ему из первого эксперимента, когда оба индекса были уникальными, то, как говорится, «разница бросается в глаза». Тогда у нас кластерный индекс был «трехэтажным», но при этом не-кластерный — лишь двух. Теперь последний догнал по «этажности» первый. А значит, к финальному практическому выводу предыдущей части материала, добавляется второй, не менее практический: к «раздуванию» data/index pages ведет не только разрешение колонкам содержать NULL значения, но и разрешению тем же колонкам содержать дубликаты. Или, обобщая, чем строже правила «входного контроля» для колонок, тем меньше места на диске эти колонки занимают. Разумеется, допустимость NULL значений или дубликатов для данной колонки определяется обыкновенно не желанием DBA, а конкретными и фиксированными законами бизнеса, чьи нужды эта колонка обслуживает. Но, как неоднократно повторял автор, отметить технический факт — это одно, по преимуществу автор этим и занимается. А найти этому факту интересное/полезное применение — это другое, чаще всего оставляемое на усмотрение читателя. Просто потому, что у каждого свои условия, системы, задачи, цели и т.п. Кому-то факт окажется совершенно бесполезен, а кто-то за него отдаст если не руку, то квартальную премию точно. :)

Как бы там ни было, но вот еще один факт не вызывающий никаких сомнений: структура кластерного индекса CL_uq_Index осталась по сравнению с тем же предыдущим экспериментом абсолютно идентичной, буквально байт-в байт. Потому как и команда создающая этот индекс такая же, и колонка по которой он создается тоже. А стало быть половина работы уже нами проведена, осталось выяснить — что же такого кардинального поменялось в индексе NONCL_uq_Index, что он себе аж целый «этаж» надстроил?

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

1
2
DBCC TRACEON(3604)
DBCC PAGE([~DB~], 1, 1061, 3) --non-cluster idx, root level(2)

Нам в ответ:

PageId  Level   ChildPageId col2 (key)  col1 (key)
1061    2       110         NULL        NULL
1061    2       1062        606350      116352

Так, любопытно! Мы не «заказывали» колонку col1 в качестве ключевой для не-кластерного индекса, но она таки стала ею без всякого нашего указания. Причем, еще раз отметьте это себе, col1 не просто «включилась» в индекс, как это было ранее на листовом уровне того же не-кластерного индекса. Нет, интеграция в этот раз имеет куда как более основательное проникновение, col1 является полноценным ключом индекса в полном соответствии с толкованием этого термина. Да и приставка (key) в имени колонки сомнений не оставляет — ключ, настоящий и стопроцентный. Разумеется, ведущей частью ключа являются значения второй колонки, col2, но, несмотря на свою «ведомость», значения первой колонки теперь являются неотъемлемой частью того же ключа.

Резонный вопрос — чем вызваны такие кардинальные изменения? Почему в предыдущем опыте col1 лишь «включалась» на листовом уровне не-кластерного индекса, а теперь она фигурирует в виде ключа, а поэтому, несомненно, будет проходить по всему индексу, сверху донизу? А потому, что есть то самое «мега-важное» правило, анонсированное автором парой абзацев выше. Вот оно:

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

Правило короткое, но слова в нем на «вес золота». «Каждая запись» — это каждая абсолютно. «Индексная страница» — это индексная любого уровня, любого индекса, но не data pages, находящиеся, как хорошо известно читателям данной статьи, исключительно на листовом уровне кластерного индекса. Ну и так далее. В связи с особой важностью правила — его иллюстрация на «живых» строчках нашей таблицы.

Возьмем вторую из записей страницы корневого уровня, то есть со страницы 1061-й приведенной чуть выше. Как показывает последний резалт-сет значение 606350 колонки col2 в этой записи символизирует, как и любое значение не-листового уровня, границу очередного диапазона. Подумаем — могла ли такая запись появиться в нашем первом эксперименте, без уточнения в виде значения 116352 первой колонки? Вполне! Потому что такое значение в col2 тогда было бесспорно единственным, и «правило №1» (обозначим его так) — выполнено. И производя поиск типа ...WHERE col2=606350 мы тогда могли на этой строчке «тормозить», ну или спускаться ниже, если нам еще col1 была бы нужна. А сейчас? А сейчас у нас таких значений, 606350, аж десять штук разных (в смысле значения-то идентичные, но содержатся они в десятке физически различных строк). И производя такой же поиск мы не можем «тормозить» нигде, ни при каких условиях. Мы обязаны идти до листового уровня, потому что col2=606350 могут оказаться и на нем тоже. Одним словом, теперь наше правило — нарушено, надо исправляться. Как? Самое интересное — как угодно! Далее мы увидим, что движок сервера исключительно «изворотлив», и умеет выходить из ситуации нарушенного правила пятком различных способов. В данном случае идея проста и очевидна: col2 не уникальна, но col1 — да. А поэтому если «пристегнуть» второе к первому, то получившаяся запись (содержащая уже комбинацию значений обеих колонок) будет снова гарантированно уникальной — все довольны. Понятно, что именно col1 на роль «уникального заполнителя» выбирается по очень простому принципу — это ключ кластерного индекса. «Ну а если такой ключ кластерного индекса будет тоже не-уникальным», спрашиваете вы? Не беспокойтесь — он будет уникальным всегда, совершенно без вариантов, как бы вы не старались движку в этом его стремлении «помешать». Читайте материал далее и вы увидите, как красиво «обманывает» нас подчас движок сервера выполняя эту свою «мета-задачу» — обеспечения безусловной уникальности для кластерного ключа.

А во что же выливается обходной маневр с «пристегиванием» col1 как «уникализатора» в смысле затратности? Судите сами: в прошлый раз первая запись каждого не-листового уровня не-кластерного (впрочем — и кластерного тоже) индекса содержалась в слоте размером 11 байт, а все последующие — в слоте размером 14 байт. Размер же слота листового уровня от порядкового номера не зависел и (для не-кластерного индекса) был всегда 12 байт. В текущем эксперименте три указанных значения составят 15, 18 и снова 12 байт соответственно. Если коротко — «плюс четыре байта» к каждому слоту на не-листовом уровне. И это еще, можно сказать, повезло. col1 мог ведь оказаться nvarchar(80) или типа того, со всеми вытекающими. Если срезюмировать выводы из наших последних открытий, причем максимально сжато, то это будет так: движок сервера очень «любит» индексы по уникальным колонкам. А по не-уникальным — не очень. :) Что, конечно, не может быть для вас препятствием для создания подобных индексов, нужно лишь четко представлять себе что, во-первых, «за все надо платить», а во-вторых — размер этой «оплаты».

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

Структура не-кластерного индекса при наличии уникального кластерного

  Не-кластерный индекс уникален Не-кластерный индекс не-уникален
Не-листовой уровень Значения кластерного индекса не фигурируют вообще Значения кластерного индекса представлены и являются неотъемлемой частью ключа не-кластерного индекса
Листовой уровень Значения кластерного индекса представлены, но являются обычной, «добавочной» колонкой к ключевой колонке не-кластерного индекса Значения кластерного индекса представлены и являются неотъемлемой частью ключа не-кластерного индекса

Наконец, самые любознательные читатели могут спросить автора: «а в чем метафизический смысл нашего правила №1? Почему движок настолько боится дубликатов в индексных записях, что для преодоления этой ситуации готов жечь ресурсы в совершенно неограниченных масштабах?». Ну, это-то как раз очень просто: движок принципиально не может допустить ситуации, когда у него нет четкого и однозначного метода доступа к любой конкретной записи. Аналогия из мира программирования десктопных приложений (если заниматься этим делом с помощью Windows API, конечно; автор совершенно не силен во всяких там Mac OS/Unix и не исключает, что в них такой подход не используется, но вот в Windows — это однозначно «рабочая лошадка» программиста) — хэндл файла (file handle). Как известно, это некий идентификатор присваиваемый операционной системой открытому файлу. А для программиста — это просто номер, который он должен указывать при любом обращении к этому файлу. И это требование понятно и резонно: мы же, очевидно, хотим записать строку символов не в любой под руку подвернувшийся файл, а именно в наш C:\log.txt, не правда ли? А любой другой файл мы трогать/изменять не хотим. И представить себе что бы два разных хэндла указывали на один и тот же файл решительно невозможно, как невозможна и обратная ситуация — один и тот же хэндл «рулит» сразу двумя различными файлами. И то и другое приведет к несомненной катастрофе. Так вот для нас, «SQL-щиков», файл — это запись индексной страницы, то есть отдельный слот на ней. А хэндл — это колонка (или колонки — это уж как получится) составляющие ключ того индекса чьи записи эта самая страница содержит. Ну или более точно — сочетание значений из указанных колонок. А дальше — «хэндл должен быть уникален». Любой ценой, иначе... В общем — аналогия понятна? Тогда отлично, мы можем переходить к «эксперименту номер три».

Не-уникальный кластерный + уникальный не-кластерный.

Теперь у нас ситуация обратная предыдущей — то есть наша пара индексов сменила характеристики уникальности на противоположные. Прежде чем переходить к практическим занятиям немного порассуждаем. Мы уже знаем что «правило №1» запрещает любому индексу, а уж тем более кластерному, иметь не то что практически эквивалентные записи, а даже иметь потенциальную возможность такие записи завести. Стало быть, для нашего третьего эксперимента ситуация совершенно прозрачна: нам нужен искусственный «уникализатор» для индекса CL_uq_Index, ровно как в прошлый раз он был нужен для NONCL_uq_Index. Подумаем — а не может ли в такой роли выступить гарантированно уникальная на сей раз col2? Ну то есть если индексы «обменялись» показателями уникальности может они, заодно, «обменяются» своими внутренними структурами? В целом можно утверждать, что данная идея — как минимум «рабочая». Так могли бы сделать. То есть кластерный индекс мог бы состоять из композитного ключа: col1 (поскольку мы его указали) + col2 (который бы мы не указывали, но который включался бы в ключ принудительно). Но делать так — не стали. Мысль разработчиков движка шла примерно таким маршрутом: это сейчас col2 уникальна. А завтра оп — и уже нет. А то и вовсе, возьмет DBA да и удалит данную колонку «с потрохами». Что тогда — перестраивать кластерный индекс с какой-нибудь уникальной col3? Но ведь кластерный индекс, как известно, в свою очередь является «эрзац-уникализатором» для всех не-кластерных индексов (ну, кроме уникальных, разумеется). Что подразумевает, при таком развитии сюжета, перестройку всех (или почти всех) не-кластерных индексов, что, понятно, на рабочей базе нам совсем, совсем «не улыбается», особенно в пиковые часы. И самое главное — а что если среди всех этих colX нет уникальных? Ну вот нет уникальной колонки и все тут, во всей таблице! Разумеется, можно было б прописать в BOL: «как минимум одна колонка любой таблицы (а равно представления, кстати говоря) обязана быть уникальной». Но это ж какие вопли пойдут... Нет, нужно более изящное решение. Такое решение, которое:

  • работало бы на таблице/представлении любой структуры;
  • было устойчивым (насколько это возможно), к изменениям этой самой структуры;
  • не принуждало бы пользователя системы к «вынужденным телодвижениям», вроде создания эрзац-колонки;
  • и т.п.

Вот, примерно такое техзадание получили в свое время программисты движка SQL Server. Что у них получилось — давайте смотреть. Очередной тестовый скрипт, третий по счету:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
USE master
go
CREATE DATABASE [~DB~]
ON PRIMARY (
    NAME = '~DB~_Data',
    FILENAME = 'c:\sqlCMD.ru\~DB~.mdf',
    SIZE = 20 MB )
LOG ON (
    NAME = '~DB~_Log',
    FILENAME = 'c:\sqlCMD.ru\~DB~.ldf',
    SIZE = 20 MB )
GO
USE [~DB~]
GO
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'T1') DROP TABLE T1
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Tsup') DROP TABLE Tsup
CREATE TABLE Tsup (PageFID  tinyint, PagePID int, IAMFID   tinyint, IAMPID  int, ObjectID  int, IndexID  tinyint, PartitionNumber tinyint,
 PartitionID bigint, iam_chain_type  varchar(30), PageType  tinyint, IndexLevel  tinyint, NextPageFID  tinyint, NextPagePID  int, PrevPageFID  tinyint, PrevPagePID int)
CREATE TABLE T1 (col1 int, col2 int)
GO
CREATE CLUSTERED INDEX CL_uq_Index ON T1(col1)
GO
CREATE UNIQUE NONCLUSTERED INDEX NONCL_uq_Index ON T1(col2)
GO
declare @i int, @j int
SET @i = 0
SET @j = 0
BEGIN TRANSACTION
    WHILE @i < 250000 --loop1 counter
    BEGIN
        WHILE @j < 10 --loop2 counter
        BEGIN
            insert into T1 VALUES (10000+@i, 500000+@i+@j)
            set @j += 1
        END
        set @i += 10
        set @j = 0
    END
COMMIT TRANSACTION
INSERT INTO Tsup EXEC ('DBCC IND ([~DB~], T1, -1)');
SELECT PagePID,IndexID,PageType,IndexLevel FROM Tsup WHERE IndexLevel>0

Все ровно наоборот по сравнению с опытом предыдущим — на 10 идентичных значений col1 приходится такое же число уникальных значений col2. Отчет команды DBCC IND сообщает нам что:

PagePID IndexID PageType    IndexLevel
94      1       2           1
850     1       2           2
851     1       2           1
1162    1       2           1
1480    1       2           1
110     2       2           1
1259    2       2           2
1260    2       2           1

Что мы можем определить сразу, «влет», без всяческих дополнительных «раскопок»? Мы можем констатировать еще более снизившуюся эффективность индекса по сравнению с предыдущим вариантом. А про вариант первый и говорить не приходится — тут разница уже скорее качественная, нежели количественная. Напомню самоочевидный факт: чем более индекс компактен, чем меньше он занимает места на диске, тем он, во-первых, собственно занимает меньше места, а во-вторых — работает более эффективно. Когда у нас оба индекса были уникальными общее их число страниц не-листовых уровней выражалось цифрой 4. Когда мы сделали не-уникальным не-кластерный индекс она возросла до 6-ти, да плюс повысилась «этажность» этого не-кластерного индекса. Ну а теперь мало того что повышенная «этажность» сохранилась, так общие число не-листовых страниц достигло 8-ми! То есть по сравнению с первым опытом увеличился в два раза. При, что самое удивительное, абсолютной идентичности объема итоговой информации содержащейся в таблице — 250 тыс. записей в две колонки, каждая типа int. Что еще раз подтверждает простое правило: чем «вольготнее» чувствует себя колонка в смысле возможных ее значений — тем более дорогую цену вы за это «заплатите», если надумаете включить ее в не-кластерный индекс. Если же надумаете ее же, но в кластерный — «ценник», грубо говоря, умножается на два.

Но — ладно, что же все-таки придумали наши бравые разработчики движка? Как обеспечили уникальность записей кластерного индекса не прибегая к помощи других колонок той же таблицы? Обратимся, к примеру, к корневой странице обсуждаемого индекса, то есть к странице 850-й:

1
2
DBCC TRACEON(3604)
DBCC PAGE([~DB~], 1, 850, 3) --cluster idx, root level(2)

И вот что мы видим:

PageId  Level   ChildPageId col1 (key)  UNIQUIFIER (key)
850     2       94          NULL        NULL
850     2       851         65440       4
850     2       1162        120550      4
850     2       1480        175660      4

Разумеется, на ближайшие 2-3 абзаца центром обсуждения станет колонка с загадочным именем UNIQUIFIER. Это вообще — что? Откуда? Зачем? У нас в таблице T1 сколько вообще колонок?? Успокойтесь, вы же видите — теперь уже три. Автор, вообще-то, предлагал оформить тоже самое в виде дополнительной строки в BOL и дать некоторые рычаги управления структурой в руки DBA, но и так можно: «насильно» добавляется колонка, с безальтернативным именем UNIQUIFIER, с особым типом (об этом через пару предложений), которая безусловно включается в ключ индекса и скрывается от просмотра любыми способами, кроме вот таких, «полу-хакерских». Ну то есть не придумалось ничего иного, кроме как добавить гарантировано уникальную колонку и вот уже с ее помощью... Имеете альтернативные предложения по решению вопроса отсутствия дубликатов в индексе? Пишите — обсудим, хотя вряд ли тут можно что-то изобрести. Можете поверить, что решение принималось в муках, далеко не за 5 минут, и является, несомненно, «меньшим из зол», а отнюдь не идеальным решением. Которого, по видимому, просто не существует. Под определенные условия можно предложить решение (а скорее несколько) много, много лучше реализованного. Но требуется-то решение под любые условия и таблицы любых структур допустимых согласно синтаксису T-SQL, и вот тут уже ничего принципиально лучшего не придумать, как ни старайся. А впрочем — дерзайте...

Мы же сосредоточимся на фактах связанных с этой интересной колонкой. Во-первых — имя. Строго говоря, у этой колонки нет вообще никакого имени, просто потому, что движок обращается к колонкам по их ID, а человек, по идее, к такой колонке обратиться не может. Так что UNIQUIFIER — не более чем «изобретательство» команды DBCC PAGE. Но поскольку нам в тексте на данную колонку надо как-то ссылаться, мы будем условно считать, что имя у нее все-таки есть. Во-вторых — тип. Тут все сложно. Опять же, факты таковы:

  • UNIQUIFIER содержит целые и только целые числа, к тому же положительные;
  • число 0 в значении этой колонки имеет особый смысл — эта цифра, во-первых, допустима, а, во-вторых, полностью эквивалентна значению NULL, что, в общем случае, совершенно неверно, нуль и NULL не имеют ничего общего. Однако для этой искусственной колонки и правила тоже искусственные. Так вот для нее 0, он же NULL, означает: данная запись страницы (слот) не нуждается в услугах «эрзац-уникализатора» UNIQUIFIER в силу того обстоятельства, что запись уникальна сама по себе. Ведь если мы в col1 вставили цифру 10000 мы можем вставить ее же еще раз, а то и несколько. Но не обязаны это делать. Так вот если пока col1=10000 — единственная, зачем ее, такую запись/ключ, еще уточнять?
  • если значение UNIQUIFIER равно 0/NULL ресурсов на ее поддержание не требуется никаких, то есть абсолютно. То есть в этом случае UNIQUIFIER существует исключительно виртуально, в нашем воображении (ну и движка тоже, разумеется);
  • начиная со значения 1 и далее само значение разбираемой колонки будет занимать не менее 4-х байт. То есть, в первом приближении, UNIQUIFIER это «как бы int»;
  • однако, как известно, таблицы баз данных обслуживаемых SQL сервером не имеют жестких рамок на число строк в них содержащихся, поэтому, предположив что нам достался удивительный, «резиновый» HDD, мы можем допустить, что таблица T1 содержит бесконечное число записей и у всех — одно и тоже значение в col1. Одним словом, UNIQUIFIER должен быть готов предложить бесконечно большое положительное число. А поэтому эта колонка имеет переменную размерность (то есть является variable length column). Пока все «запихивается» в 4 байта — так и делается. Исчерпалось место — увеличиваем до 8-ми и т.д.;
  • если значение UNIQUIFIER отлично от 0/NULL, то независимо от истинного размера этой колонки еще несколько байт (строго говоря — четыре) уйдут на поддержку функциональности «плавающего» размера. То есть каждая запись на странице индекса/данных будет занимать на 4 байта больше, чем мы могли бы подумать будучи уже осведомленными о существовании колонки UNIQUIFIER как таковой.

Разумеется, мы не можем не подкрепить сухие факты реальными значениями нашей «живой» тест-таблицы. Вот, допустим, первая пара записей с root-страницы (т.е. это записи ссылающиеся на дочерние страницы 94-ю и 851-ю) в байтовом представлении (а те же самые записи в табличном представлении вы можете видеть чуть выше). Сначала запись ведущая к 94-й странице:

06 411f0600 5e000000 0100

Поскольку тут мы имеем дело с известным особым случаем «левой границы первого диапазона», то дешифровка будет слово в слово совпадать с таковой для предыдущего нашего опыта. Как и тогда, эта «особая» запись занимает 11 байт и, по сути, содержит один единственный «кусок» информации — ссылку на дочернюю страницу (0x5e=94). Ни колонок реальных, ни колонок искусственных, ни их значений нет и в помине. Но, опять же, это — «особый» случай.

Совсем иные «песни» начинаются для «нормальных» записей, первой из которых будет как раз запись с ссылкой на страницу 851-ю. Вот слот этой записи:

36 a0ff0000 53030000 0100 0100 00 0100 1600 04000000

Смысл записи такой:

  • 36 — все тот же статус, однако, в отличии от предыдущих экспериментов, он говорит нам не только о том, что запись индексная, и не только о том, что в ней присутствует NULL bitmap, но и о наличии в записи колонки (как минимум одной) переменной размерности. Сразу за байтом статуса начинаются колонки фиксированной размерности;
  • a0ff0000 — или 65440, граница диапазона, она же значение колонки col1;
  • 53030000 и 0100 — или 851 и 1, страница и, соответственно, файл где она находится, то есть ссылка на дочернюю страницу;
  • 0100 и 00 — число колонок фиксированной размерности и отметка наличия в какой-нибудь из них значения NULL, т.е. NULL bitmap. В нашем случае col1 таковых значений не имеет;
  • 0100 — число колонок «плавающей» размерности. У нас такая одна;
  • 1600 — или 22. Число байт от начала слота где закончится (а не начнется!) первая колонка переменной размерности. У нас она закончится вместе со слотом, а поэтому данное поле просто указывает на последний байт записи;
  • 04000000 — или просто 4. Значение UNIQUIFIER, разумеется.

Итого, в данной записи значение колонки col1 равно 65440, а вот значение ключа — 65440+4. Только складывать ничего не нужно, в данном случае «плюс» это не арифметическое действие, а сочетание двух значений. И с затратностью такого подхода все ясно. Пока мы в «уникализаторе» не нуждались запись кластерного индекса на любой не-листовой странице занимала 14 байт, а теперь — 22. Вот вам и «плата» за «свободу выбора». Восемь байт дополнительно на каждую строку таблицы без реального увеличения ее информационной составляющей — это уже очень, очень серьезно. И только пара моментов дают нам хоть какой-то заряд оптимизма:

  • любая первая запись на каждом уровне индекса остается без изменений и по прежнему занимает 11 байт;
  • накладные расходы в виде 8-ми дополнительных байт (и это еще хорошо, если только 8, в экстремальных случаях и побольше может быть) случаются только если обнаружено реальное дублирование записей. Пока в формально не-уникальном индексе значение все-таки остается уникальным по факту — все хорошо, никаких дополнительных расходов мы не несем.

Первый пункт этого «утешительного списка» не слишком-то утешает. Ну сколько там этих уровней? Ну пусть даже 500 (это уже должна быть просто огромная таблица, что бы такой «небоскреб» возвести). Значит 500 раз мы ограничимся 11-ю байтами вместо 22-х. «Чистый доход» равен 5.5Кб. С учетом что таблица однозначно будет занимать гигабайты — ерунда, а не экономия. Второй пункт выглядит более многообещающим, и его реализацию мы посмотрим на примере первых двух записей первой страницы листового уровня все того же кластерного индекса. Мы уже знаем, что первой дочерней страницей является 94-я. А какая страница является первой дочерней для нее? Это мы умеем:

1
2
DBCC TRACEON(3604)
DBCC PAGE([~DB~], 1, 94, 3) --cluster idx, non-leaf level(1)

И выясняется, что (на машине автора) первой листовой страницей будет страница 80. А как выглядят первые две записи этой страницы? Очевидно, это первые две строки нашей таблицы, то есть col1=10000 для обеих, а col2=500000 для первой и col2=500001 для второй. Ну и конечно тут везде будет UNIQUIFIER, а как же! Это же ключ, как его можно в записи не упомянуть? А что насчет значений этой колонки-ключа? Можем мы их предсказать? Ну, можем хотя бы попытаться. Мы уже знаем, что «уникализация» начинается с нуля, он же NULL. Наверно, для самой первой строки такое значение в UNIQUIFIER и будет. А для второй? Очевидно, на 1 больше. Вот давайте начнем с этой второй записи, то есть там где UNIQUIFIER уже не может быть NULL. Вот ее слот, если посмотреть его с помощью все той же недокументированной команды DBCC PAGE:

30000c00 10270000 21a10700 0300 00 0100 1700 01000000

А вот — «дешифровка»:

  • 30000c00 — заголовок строки. Важное для нас:
    • запись — данные (а не индекс!);
    • NULL bitmap — присутствует;
    • колонки переменной размерности — присутствует хотя бы одна;
  • 10270000 — или 10000, col1;
  • 21a10700 — или 500001, col2;
  • 0300 — общее число колонок, 3. Да, автор заметил, что в предыдущем разборе слота страницы корневого уровня в этом месте значилось число колонок фиксированного размера, а не их общее количество. А вы заметили, что тогда слот относился к index page, а разбираемый в настоящий момент — к data page? Разве кто-то обещал 100% совпадение структур записей и смысла их значений?
  • 00 — NULL bitmap. Иными словами ни одна колонка NULL не содержит;
  • 0100 — число колонок «плавающей» размерности, то есть таковых у нас одна. Еще раз обратите внимание, что в отличии от индексной страницы, тут у нас «три, в том числе одна», а не «три, и еще одна», как было тогда;
  • 1700 — или 23. Тоже самое что ранее: число байт от начала слота где закончится первая колонка переменной размерности. У нас она, опять же, закончится вместе со слотом;
  • 01000000 — или 1. Значение этой самой единственной колонки. Как мы и предполагали, UNIQUIFIER для этой второй записи равен единице.

Снова, во времена уникального кластерного индекса каждый слот листового уровня занимал 15 байт, а теперь — 23. Не слабо! :idea: Но обратим внимание вот на какую деталь: при вставке только что разобранной записи движок сервера точно знал, что ключ индекса — дублируется. Он же «видел» самую первую запись, с тем же значением в колонке col1! А на момент вставки этой первой? А вот тогда col1=10000 была еще уникальной сама по себе, и никаких «телодвижений» не требовала.