23.9.25

Использование DBCC PAGE и DBCC IND для доказательства, что расщепление не откатывается

Автор: Paul Randal, Inside the Storage Engine: Using DBCC PAGE and DBCC IND to find out if page splits ever roll back

Прежде чем переходить к механизму работы, хочу разобрать две команды, которые я буду часто использовать — DBCC PAGE и DBCC IND. Обе они (на момент написания статьи) являются недокументированными и не поддерживаются официально, но абсолютно безопасны в применении, так как активно используются внутри и вне Microsoft при диагностике. Тем не менее, используйте их на свой страх и риск. Эти команды хорошо известны в сообществе MSSQL, я и другие специалисты уже рассказывали о них ранее.

Чтобы показать, как они работают, я приведу простой сценарий, доказывающий, что расщепления страниц никогда не откатываются. У меня был спор на эту тему, и ответ всегда один — нет. Расщепление страницы происходит тогда, когда вставка или обновление должны произойти в определённом месте индексной страницы, но там не хватает места для новой или обновлённой записи. Расщепления выполняются как отдельные «системные» транзакции. После того как системная транзакция зафиксирована, она не может быть отменена — даже если откатывается пользовательская транзакция, частью которой она была.

Сначала создадим базу данных с таблицей и индексом (так как расщепления происходят только в индексах).


USE MASTER;
GO

IF DATABASEPROPERTY (N'pagesplittest', 'Version') > 0 DROP DATABASE pagesplittest;
GO

CREATE DATABASE pagesplittest;
GO

USE pagesplittest;
GO

CREATE TABLE t1 (c1 INT, c2 VARCHAR (1000));
CREATE CLUSTERED INDEX t1c1 ON t1 (c1);
GO
  

Теперь заполним индексную страницу так, чтобы осталось «окно» для вставки недостающего значения и можно было вызвать расщепление.


INSERT INTO t1 VALUES (1, REPLICATE('a', 900));
INSERT INTO t1 VALUES (2, REPLICATE('b', 900));
INSERT INTO t1 VALUES (3, REPLICATE('c', 900));
INSERT INTO t1 VALUES (4, REPLICATE('d', 900));
-- пропуск значения 5
INSERT INTO t1 VALUES (6, REPLICATE('f', 900));
INSERT INTO t1 VALUES (7, REPLICATE('g', 900));
INSERT INTO t1 VALUES (8, REPLICATE('h', 900));
INSERT INTO t1 VALUES (9, REPLICATE('i', 900));
GO
  

Использование DBCC IND

Чтобы узнать, какая страница является первой в индексе, используем команду DBCC IND:


DBCC IND ('pagesplittest', 't1', 1);
GO
  

Эта команда выводит список всех страниц, выделенных под индекс. Пример вывода:

Расшифровка колонок:

  • PageFID — идентификатор файла страницы
  • PagePID — номер страницы в файле
  • IAMFID — идентификатор файла страницы IAM
  • IAMPID — номер страницы IAM
  • ObjectID — идентификатор объекта
  • IndexID — идентификатор индекса
  • PartitionNumber — номер секции
  • PartitionID — внутренний ID секции
  • iam_chain_type — тип IAM-цепочки
  • PageType — тип страницы (1 — данные, 2 — индекс, 8 — GAM, 9 — SGAM, 10 — IAM, 11 — PFS и т. д.)
  • IndexLevel — уровень страницы в индексе (0 — листья, N — корень)
  • NextPageFID/NextPagePID, PrevPageFID/PrevPagePID — ссылки на соседние страницы

Видим, что у нас одностраничный кластеризованный индекс с IAM-страницей. Заметьте, что ID страниц у вас могут быть другими. Давайте посмотрим данные страницы:


DBCC TRACEON (3604);
GO
DBCC PAGE (pagesplittest, 1, 143, 3);
GO  

Флаг трассировки позволяет выводить данные команды DBCC PAGE на консоль, а не в журнал ошибок. Синтаксис команды DBCC PAGE:

dbcc page ( {‘dbname’ | dbid}, filenum, pagenum [, printopt={0|1|2|3} ])

Параметры filenum и pagenum берутся из идентификаторов страниц, которые берутся из различных системных таблиц и отображаются в DBCC или других системных сообщениях об ошибках. Пот пример расшифровки идентификатора страницы: (1:143) будет filenum = 1 и pagenum = 143.

Параметр printopt имеет следующие значения:

0 — вывести только заголовок страницы
1 — заголовок страницы плюс шестнадцатеричные дампы для каждой строки и дамп массива слотов страницы (если только это не страница без слота, например, битовая карта распределения)
2 — заголовок страницы плюс шестнадцатеричный дамп всей страницы
3 — заголовок страницы плюс подробное описание каждой строки

Интерпретация по строкам работает для всех типов страниц, включая битовые карты распределения. В нашем случае мы запросили подробный вывод. Я объясню, что означают различные части вывода, в посте об анатомии страницы. Вот вывод команды DBCC PAGE, из которого для краткости удалена часть повторяющейся информации по строкам:

PAGE: (1:143) 

BUFFER: 

BUF @0x02C49720 
bpage = 0x05400000                   bhash = 0x00000000                   bpageno = (1:143)
bdbid = 8                            breferences = 0                      bUse1 = 22163
bstat = 0xc0010b                     blog = 0x32159bb                     bnext = 0x00000000 
PAGE HEADER: 

Page @0x05400000 
m_pageId = (1:143)                   m_headerVersion = 1                  m_type = 1
m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x8000
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 = 8                          m_slotCnt = 8                        m_freeCnt = 744
m_freeData = 7432                    m_reservedCnt = 0                    m_lsn = (18:113:2)
m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0
m_tornBits = 0                       
Allocation Status 
GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED               
PFS (1:1) = 0x60 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED            
Slot 0 Offset 0x60 Length 917 
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS 
Memory Dump @0x5C24C060 
00000000:   30000800 01000000 0300f802 00110095 †0...............         
00000010:   03616161 61616161 61616161 61616161 †.aaaaaaaaaaaaaaa         
<snip> I've removed this section to save space 
00000380:   61616161 61616161 61616161 61616161 †aaaaaaaaaaaaaaaa         
00000390:   61616161 61††††††††††††††††††††††††††aaaaa                    
UNIQUIFIER = [NULL]                  
Slot 0 Column 1 Offset 0x4 Length 4 
c1 = 1                               
Slot 0 Column 2 Offset 0x11 Length 900 
c2 = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a                                    
Slot 1 Offset 0x3f5 Length 917 
Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS 
Memory Dump @0x5C24C3F5 
<snip> And again... 
DBCC execution completed. If DBCC printed error messages, contact your system administrator. > >

Вывод показывает, что каждая строка занимает 917 байт, свободно только 744 байта (m_freecnt в заголовке страницы). Значит, вставить новую строку аналогичной длины на эту страницу невозможно — но именно это мы и собираемся сделать! Помните, что на этой странице сейчас девять строк. Вызовем расщепление:


BEGIN TRAN;
GO

INSERT INTO t1 VALUES (5, REPLICATE('a', 900));
GO
  

Мы знаем, что места не было, значит страница должна была расщепиться. Проверим снова DBCC IND:

Добавились две страницы — одна индексная и ещё одна страница данных. До этого индексу требовалась только одна страница, теперь — две. Поэтому создаётся индексная страница для поддержки поиска. Анализ страниц показывает: страница (1:143) содержит значения с1 от 1 до 5, страница (1:154) — значения с1 от 6 до 9. Это ожидаемо: расщепление произошло в точке вставки, и новая строка оказалась на расщепившейся странице.

Теперь откатим транзакцию:

ROLLBACK TRAN;
GO
  

После отката структура индекса остаётся такой же, как после расщепления. Страница (1:154) хранит строки, полученные при расщеплении, а страница (1:143) содержит остальные строки, но без той, что была вставлена в рамках транзакции.

Таким образом, мы доказали: расщепление страниц никогда не откатывается.

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

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