25.9.25

Страницы IAM, цепочки IAM и единицы распределения

Автор: Paul Randal, Inside the Storage Engine: IAM pages, IAM chains, and allocation units

Это компиляция из ранее опубликованного материала с добавлением новых фрагментов вывода DBCC PAGE.

Страницы IAM

Страница IAM (Index Allocation Map) отслеживает примерно 4 ГБ пространства в одном файле, выровненных по границе 4 ГБ. Эти 4‑гигабайтные фрагменты называются «интервалами GAM». Страница IAM отслеживает, какие экстенты внутри конкретного интервала GAM принадлежат одной сущности (я тщательно подбираю слова и сознательно не использую терминов с коннотациями (connotation) SQL Server вроде «объект»).

Примечание переводчика: Connotation — это дополнительное значение или эмоциональная ассоциация слова, выходящая за рамки его словарного определения. Думаю, тут Пол имел ввиду отрицательные эмоции, когда мы встречаем в тексте сухие термины, которые могут означать очень многие объекты и/или сущности...

Одна страница IAM может отслеживать пространство только для одного интервала GAM в одном файле, поэтому если в базе данных несколько файлов или некоторые файлы больше 4 ГБ, а у сущности есть выделенное пространство из нескольких файлов или из нескольких интервалов GAM в одном файле, то становится понятно, почему каждой сущности требуется несколько страниц IAM, чтобы отследить всё используемое пространство. Если сущности необходимо несколько страниц IAM для отслеживания всех её экстентов, то страницы IAM должны быть связаны между собой. Здесь и появляется цепочка IAM. Подробнее об этом ниже.

Каждая страница IAM содержит две записи: заголовок страницы IAM и битовую карту. Давайте взглянем на пример с DBCC PAGE. Я использую базу данных из статьи о расщеплении страниц (page split). Выполнение DBCC IND для созданной таблицы даёт нам следующее:

Посмотрев на колонку PageType, видно, что существует страница IAM (страница с типом 10 — см. статью «Анатомия страницы» для подробностей) с идентификатором страницы (1:152):

> DBCC TRACEON (3604);
> GO
> 
> DBCC PAGE (‘pagesplittest’, 1, 152, 3);
> GO
> 
> m_pageId = (1:152)                   m_headerVersion = 1                  m_type = 10  
> m_typeFlagBits = 0x0                 m_level = 0                          m_flagBits = 0x200  
> m_objId (AllocUnitId.idObj) = 68     m_indexId (AllocUnitId.idInd) = 256  
> Metadata: AllocUnitId = 72057594042384384  
> Metadata: PartitionId = 72057594038386688                                 Metadata: IndexId = 1  
> Metadata: ObjectId = 2073058421      m_prevPage = (0:0)                   m_nextPage = (0:0)  
> pminlen = 90                         m_slotCnt = 2                        m_freeCnt = 6  
> m_freeData = 8182                    m_reservedCnt = 0                    m_lsn = (18:116:13)  
> m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0  
> m_tornBits = -1947725876
> 
> Allocation Status
> 
> GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED  
> PFS (1:1) = 0x70 IAM_PG MIXED_EXT ALLOCATED   0_PCT_FULL                  DIFF (1:6) = CHANGED  
> ML (1:7) = NOT MIN_LOGGED
> 
> IAM: Header @0x620CC064 Slot 0, Offset 96
> 
> sequenceNumber = 0                   status = 0x0                         objectId = 0  
> indexId = 0                          page_count = 0                       start_pg = (1:0)  
> IAM: Single Page Allocations @0x620CC08E
> 
> Slot 0 = (1:143)                     Slot 1 = (1:153)                     Slot 2 = (1:154)  
> Slot 3 = (0:0)                       Slot 4 = (0:0)                       Slot 5 = (0:0)  
> Slot 6 = (0:0)                       Slot 7 = (0:0)  
> IAM: Extent Alloc Status Slot 1 @0x620CC0C2
> 
> (1:0)        – (1:272)      = NOT ALLOCATED

Несколько моментов, на которые стоит обратить внимание в самом заголовке страницы:

  • Тип страницы — 10, как и ожидалось
  • Указатели на предыдущую и следующую страницы равны NULL, так как в этой цепочке IAM других страниц IAM нет
  • Счётчик слотов равен 2 — один слот для записи заголовка IAM, один — для самой битовой карты
  • Страница почти полностью заполнена

Заголовок страницы IAM содержит следующие поля:

  • sequenceNumber
    • Позиция страницы IAM в цепочке IAM. Увеличивается на единицу при добавлении каждой новой страницы в цепочку.
  • status
    • Не используется.
  • objectId
  • indexId
    • В SQL Server 2000 и более ранних версиях это идентификаторы объекта и индекса, частью которых является страница IAM. В SQL Server 2005 и более поздних — не используются.
  • page_count
    • Не используется — ранее это было число идентификаторов страниц, отслеживаемых в массиве одиночных страничных распределений.
  • start_pg
    • Интервал GAM, который отображает данная страница. Хранит идентификатор первой страницы в отображаемом интервале.
  • Массив Single Page Allocations
    • Страницы, выделенные из смешанных экстентов по одной странице. Этот массив используется только на первой странице IAM в цепочке (так как всей цепочке IAM нужно отследить максимум 8 одиночных распределений).

Битовая карта занимает остальную часть страницы IAM и содержит по одному биту на каждый экстент в интервале GAM. Бит установлен, если экстент выделен сущности, и сброшен, если нет. Очевидно, две страницы IAM, отображающие один и тот же интервал GAM для разных сущностей, не могут иметь установлен один и тот же бит — это проверяется DBCC CHECKDB. В приведённом выше выводе DBCC PAGE видно, что таблице не выделено ни одного экстента. Обратите внимание, что вывод заканчивается экстентом, начинающимся со страницы 272 в файле — это потому, что файл данных всего такого размера. Я вставил в таблицу ещё некоторое количество строк и снова выполнил DBCC PAGE для страницы IAM. На этот раз вывод DBCC PAGE содержит:

> IAM: Single Page Allocations @0x620CC08E
> 
> Slot 0 = (1:143)                     Slot 1 = (1:153)                     Slot 2 = (1:154)  
> Slot 3 = (1:155)                     Slot 4 = (1:156)                     Slot 5 = (1:157)  
> Slot 6 = (1:158)                     Slot 7 = (1:159)  
> IAM: Extent Alloc Status Slot 1 @0x620CC0C2
> 
> (1:0)        – (1:152)      = NOT ALLOCATED  
> (1:160)      – (1:296)      =     ALLOCATED  
> (1:304)      – (1:400)      = NOT ALLOCATED

Видно, что массив одиночных распределений заполнен целиком, после чего начались распределения целых (выделенных) экстентов. Первым доступным оказался экстент, начинающийся со страницы 160, и теперь выделены все экстенты вплоть до экстента, начинающегося со страницы 296. Заметьте также, что файл, должно быть, вырос, поскольку вывод теперь доходит до страницы 400.

Ещё пара замечаний о страницах IAM:

  • Они сами являются одиночными страничными распределениями из смешанных экстентов и нигде отдельно не отслеживаются
  • Их можно распределить из экстентов любого файла для отслеживания экстентов в другом файле

Цепочки IAM

Если бы мы продолжили увеличивать файл и заполнять таблицу, то в какой‑то момент потребовалась бы ещё одна страница IAM для отображения следующего интервала GAM. Здесь вступает в дело цепочка IAM. Это связный список страниц IAM, которые отслеживают пространство, выделенное одной сущности. Связный список не упорядочен — страницы IAM добавляются в том порядке, в котором они нужны. Страницы IAM внутри списка нумеруются также в порядке добавления в список.

Определение «сущности» — что использует цепочку IAM? В SQL Server 2000 и 2005 это сильно различается. Начиная с SQL Server 2005 многое изменилось. Цепочки IAM и страницы IAM остались прежними, но теперь они соответствуют другим сущностям. Таблица может иметь до 45 миллионов цепочек IAM! Существует три вида, для которых цепочки IAM отображают распределения пространства:

  1. Кучи и b‑деревья (b‑дерево — внутренняя структура хранения индекса)
  2. LOB‑данные
  3. Данные, вынесенные за пределы строки (row‑overflow)

Эти единицы отслеживания распределения пространства принято называть «единицами распределения» (allocation units). Внутренние названия этих трёх типов единиц распределения (соответственно):

  1. Единица распределения hobt (Heap Or B‑Tree, произносится «хоббит», да, как во «Властелине колец»)
  2. Единица распределения LOB
  3. Единица распределения SLOB (Small‑LOB или Short‑LOB)

А внешние названия, соответственно:

  1. Единица распределения IN_ROW_DATA
  2. Единица распределения LOB_DATA
  3. Единица распределения ROW_OVERFLOW_DATA

Их уже некорректно было бы называть цепочками IAM, потому что они больше не отслеживают распределение пространства для индекса. Тем не менее, сама цепочка из страниц IAM по‑прежнему называется цепочкой IAM, а единица отслеживания теперь называется единицей распределения. Кроме этого, различий нет.

Кратко рассмотрим три причины, которые сделали эти изменения необходимыми и увеличили потенциальное число цепочек IAM на таблицу.

Included столбцы

Это возможность для некластерных индексов включать в листовой уровень неключевые столбцы. Это полезно по трём причинам:

  1. Позволяет некластерному индексу полностью покрывать запрос, если результаты запроса включают более 16 столбцов или суммарная длина столбцов в результатах превышает 900 байт (помним, что ключ некластеризованного индекса ограничен 16 столбцами и 900 байтами).
  2. Позволяет включать в некластеризованный индекс столбцы с типами данных, которые не могут входить в ключ индекса (например, varchar(max) или XML).
  3. Позволяет индексу покрывать запрос, не включая все столбцы в ключ индекса. Поскольку ключ индекса присутствует в строках на всех уровнях b‑дерева, это делает индекс меньше.

Пример экономии места: представим индекс на 100 миллионов строк с длиной ключа 900 байт, но реально в ключе нужны лишь первые два целочисленных столбца; ещё 4 столбца фиксированной длины можно хранить как включаемые столбцы. При ключе 900 байт на страницу помещается 8 строк (то есть разветвлённость, fanout, равна 8). Это означает 12 500 000 страниц на листовом уровне, 1 562 500 страниц на следующем уровне и так далее, в сумме 12 500 000 + 1 562 500 + 195 313 + 24 415 + 3 052 + 382 + 48 + 6 + 1 = 14 285 717 страниц (включая 1 785 717 страниц для верхних уровней b‑дерева).

Если использовать включаемые столбцы, размер ключа сокращается до 8 байт, и с учётом служебных накладных расходов можно добиться длины строки на верхних уровнях b‑дерева около 15 байт (давая разветвлённость примерно 537). Заметьте, что разветвлённость на листовом уровне останется равной 8, поскольку объём данных в каждой строке на листе тот же. Значит, на листовом уровне по‑прежнему будет 12 500 000 страниц, на следующем уровне — 23 278 страниц и так далее, в сумме 12 500 000 + 23 278 + 44 + 1 = 12 523 323 страниц (включая 23 323 страницы для верхних уровней b‑дерева). По сравнению с полным ключом в 900 байт это экономия в 12% — 1 762 394 страницы, или 13,6 ГБ! Конечно, пример несколько искусственный, но он показывает, как может возникать экономия.

Главная причина добавления этой возможности — обеспечить действительно покрывающие запросы. Покрывающий запрос — это такой, при котором оптимизатор знает, что все результаты запроса можно получить из некластерного индекса, и значит запрос можно выполнить без дополнительных операций ввода‑вывода для поиска данных в базовой таблице — значительная экономия на производительности.

Теперь, когда некластерные индексы могут иметь включаемые столбцы, и эти столбцы могут быть LOB‑типов данных (но только новых в SQL Server 2005 — varchar(max), nvarchar(max), varbinary(max) и XML), иметь единственный LOB‑контур распределения (как в SQL Server 2000 с единственным «текстовым индексом») уже нельзя, потому что у каждого индекса может быть собственный набор LOB‑значений. Можно спросить: почему бы не иметь один набор LOB‑значений с несколькими ссылками на него из разных индексов и базовой таблицы? Мы рассматривали такой вариант при разработке SQL Server 2005, но это сильно усложнило бы систему.

Итак, с этой новой возможностью каждому индексу нужны две единицы распределения — одна для строк данных или индекса (единица hobt) и одна для любых LOB‑данных.

Длинные строки

Одной из давних проблем проектировщиков схем было ограничение в 8060 байт на размер строки таблицы, поэтому в SQL Server 2005 это ограничение было снято. Это реализовано так: столбцы переменной длины (например, varchar, sqlvariant) могут быть вынесены за пределы строки, когда её размер становится слишком велик для размещения на одной странице.

Но куда переносятся эти значения? По сути, они превращаются в мини‑LOBы. Значение столбца в строке заменяется указателем длиной 24 байта (а возможно 36, 48, 60 или 72 байта) на вынесенное значение, которое хранится как LOB‑значение в отдельной единице распределения — row‑overflow (или SLOB). Эти значения хранятся на LOB‑страницах точно так же, как обычные LOB‑значения, только в отдельной единице распределения. Единица распределения SLOB создаётся только тогда, когда первое значение столбца выносится за пределы строки.

Эта возможность действует и для некластерных индексов — учитывая включаемые столбцы, вполне могут получиться строки некластерного индекса, которые не поместятся на страницу. Было бы недальновидно отменить ограничение в 900 байт и заменить его ограничением в 8060 байт, не распространив механизм вынесения за пределы строки и на некластерные индексы.

С добавлением этой возможности у каждого индекса может быть до трёх единиц распределения — hobt, LOB и SLOB. Даже так это даёт максимум 750 цепочек IAM на таблицу (помним, что теперь цепочка IAM отображает хранилище для единицы распределения: 250 индексов × 3 единицы распределения = 750 цепочек IAM) и 3000 цепочек IAM на таблицу для SQL Server 2008 и новее (где число индексов на таблицу может быть 1000). Но раньше я упомянул 45 миллионов цепочек IAM на таблицу — откуда берутся остальные?

Секционирование

Секционирование даёт множитель 15 000×. Оно позволяет делить таблицы и индексы на диапазоны, причём каждый диапазон хранится отдельно (чаще всего в отдельных файловых группах). Тема секционирования заслуживает отдельной публикации.

Если каждый диапазон, или секция, таблицы или индекса хранится отдельно, то каждой секции понадобится собственная единица hobt. Разумеется, LOB‑значения, относящиеся к каждой секции, должны храниться вместе с ней, значит, каждой секции также нужна единица LOB. Кроме того, механизм вынесения за пределы строки работает на уровне строк, и значит строки в каждой секции будут выноситься в единицы SLOB так же, как и в несекционированных таблицах и индексах. Таким образом, каждая секция таблицы или индекса может иметь до трёх единиц распределения (и, следовательно, три цепочки IAM).

И всё же откуда берётся число 15 000? Начиная с SQL Server 2008 SP2, у каждой таблицы или индекса может быть до 15 000 секций (ранее — до 1000). Это даёт 1000 индексов × 15 000 секций × 3 единицы распределения = 45 миллионов цепочек IAM. На практике это вряд ли случится, но теоретически возможно.



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

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