初步瞭解Data Record結構

本篇主要試著拆解Data Page中的Data Record,並大致說明主要會分成哪幾個部分,先前在"資料分頁(Data Page)中的剩餘空間(m_freeCnt)是如何計算出來的?"內文中,有提到關於Row Overhead的部分,所以文章最後一併指出哪些部分會被視為Row Overhead。

參考一些網路文章,並經過實際測試後,大致將Data Record的結構整理成下圖:

為了後續的驗證,首先,建立一個資料表"ExampleTab",在此不建立叢集索引,前三個欄位是[可變動長度欄位](destination、activity1、activity2),後三個欄位是[固定長度欄位](duration1、duration2、duration3),且此兩種類型的欄位各有一個欄位是允許NULL(即activity2、duration1),接著寫入三筆資料,以方便後續說明。

CREATE DATABASE RecordAnatomy
GO

USE [RecordAnatomy]
GO

CREATE TABLE [dbo].[ExampleTab](
	[destination] [varchar](100) NOT NULL,
	[activity1] [varchar](100) NOT NULL,
	[activity2] [varchar](100) NULL,
	[duration1] [int] NULL,
	[duration2] [bigint] NOT NULL,
	[duration3] [bigint] NOT NULL)
GO

INSERT [dbo].[ExampleTab] ([destination], [activity1], [activity2], [duration1], [duration2], [duration3]) VALUES (N'dest01', N'sailing01', N'sightseeing01', 1, 2, 3)
GO
INSERT [dbo].[ExampleTab] ([destination], [activity1], [activity2], [duration1], [duration2], [duration3]) VALUES (N'dest02', N'sailing02', NULL, 4, 5, 6)
GO
INSERT [dbo].[ExampleTab] ([destination], [activity1], [activity2], [duration1], [duration2], [duration3]) VALUES (N'dest03', N'sailing03', N'sightseeing03', NULL, 8, 9)
GO

執行DBCC IND指令,我們可知道ExampleTab資料表使用了1個IAM Page(PageType=10)以及1個Data Page(PageType=1),而我們主要是看Data Page,也就是Page 118的部分

DBCC IND('RecordAnatomy','ExampleTab',1)

對於執行以下DBCC PAGE後的輸出結果,我們來看Page 118的Slot 0區塊(即寫入的第一筆Record),該Record的長度為63 bytes,紅色框線的部分為十六進位表示的Data Record

DBCC TRACEON(3604)
GO
DBCC PAGE('RecordAnatomy',1,118,3)
GO

以下開始說明整個Data Record的組成,可自行對照本文前面的Data Record結構圖來看

第一個部分:Status 1 (範圍: byte 0)

Data Record的byte 0(即上圖的0x30)主要是記錄Record Types及Record Attributes,請利用"小算盤"工具,將十六進位的0x30轉成二進位值,即"00110000",順序從右至左,也就是從Bit 0至Bit 7的用途說明如下:

Bit 0:記錄是否有Version Information,但是目前在SQL Server 2008以後好像都以0表示

Bit 1~3:此連續三個Bit用來記錄該筆Record屬於何種Record Type,如底下說明,一般常見的為Primary Record及Index Record;執行資料刪除時,會短暫時間出現Ghost Data Record這種類型的Record Type而若在Heap Table中啟用Row-Versioning Isolation Level並執行資料刪除時,會產生Ghost Version Record(當Page空間尚足夠存放Version Information時),或者是Forward Stub + Forward Record(當Page空間已不足存放Version Information時),以上兩個例子有時間再另開篇章說明。 

000=0 -> Primary Record -- 在heap或clustered index的leaf-level
001=1 -> Forward Record
010=2 -> Forward Stub
011=3 -> Index Record
100=4 -> BLOB Fragment(或Row Overflow Data)
101=5 -> Ghost Index Record
110=6 -> Ghost Data Record
111=7 -> Ghost Version Record -- 此類型Record的大小為Header(1 byte) + Versioning Tag(14 bytes) 

Bit 4~7:記錄Record Attributes

Bit 4 (0x10): 若為1,表示有NULL_BITMAP屬性
Bit 5 (0x20): 若為1,表示有VARIABLE_COLUMNS屬性
Bit 6 (0x40): 若為1,表示此Record有包含VERSIONING_INFO屬性(即Versioning Information)
Bit 7 (0x80): Not used

第二個部分:Status 2 (範圍: byte 1)

使用1個byte來表示此Record是否包含FORWARDED_GHOST屬性,此屬性常出現在Rrcord Type為Forward Record的Rrcord中

0x00 -> 表Record Attribute無FORWARDED_GHOST屬性
0x01 -> 表Record Attribute有FORWARDED_GHOST屬性

第三個部分:Length of fixed length portion of record (範圍: byte 2 ~ byte 3)

此部分用2個bytes來記錄[固定長度欄位]的資料最後是儲存到第幾個byte,此例,請將"1800"轉為十六進位表示的"0x0018",再轉成以十進位表示的"24",指出[固定長度欄位]的資料只使用到byte 23,而byte 0 ~ byte 3是用來記錄Record的type及attribute,所以實際儲存[固定長度欄位]資料的是byte 4 ~ byte 23

第四個部分:Fixed Length Data (範圍: byte 4 ~ byte 23)

從前面第三部分的說明可知,byte 4 ~ byte 23這段範圍(共20個bytes),是用來儲存[固定長度欄位]的值,即欄位duration1(4 bytes) + duration2(8 bytes) + duration3(8 bytes) = 20 bytes,此部分的長度會視CREATE TABLE的定義來決定,以下來驗證各個[固定長度欄位]的值:

byte 4 ~ byte 7 ( INT資料型別占用4個bytes ):將"01000000"轉為十六進位表示的"0x00000001",再轉成以十進位表示的"1",所以duration1欄位的存放數值為"1"
byte 8 ~ byte 15 ( BIGINT資料型別占用8個bytes ):將"02000000 00000000"轉為十六進位表示的"0x0000000000000002",再轉成以十進位表示的"2",所以duration2欄位的存放數值為"2"
byte 16 ~ byte 23 ( BIGINT資料型別占用8個bytes ):將"03000000 00000000"轉為十六進位表示的"0x0000000000000003",再轉成以十進位表示的"3",所以duration3欄位的存放數值為"3"

 

第五個部分:Total number of columns in the record (範圍: byte 24 ~ byte 25)

放置完所有[固定長度欄位]的資料後,緊接著用2個bytes來記錄此筆Record的的總欄位數量,將"0600"轉為十六進位表示的"0x0006",再轉成以十進位表示的數值"6",所以此Record共有6個欄位

第六個部分:NULL bit map (範圍: byte 26)

上圖這一個byte是用來描述哪些欄位的值目前是為NULL,因為第一筆資料並沒有寫入NULL值的欄位,所以其值為"0x00"。下圖以第二筆資料(即Slot 1)來說明,請將"04"轉為十六進位表示的"0x04",再轉成以二進位表示的"00000100",從右至左依序代表第1至第8個欄位(欄位順序依實際Slot的排序而定,若資料表有設定Primary Key,則Slot中的欄位順序會先依照Primary Key欄位來排序,其餘欄位再接著按照CREATE TABLE的定義來排序),若其值為"1",表示該欄位為NULL值,所以可得知第二筆資料的第三個欄位(即activity2欄位)正存放著NULL值。視Record的欄位數量會使用1個以上的bytes來記錄,例如若有9~16個欄位,就會使用2個bytes來記錄。

第七個部分:Number of variable length column (範圍: byte 27 ~ byte 28)

此部分用2個bytes來記錄[可變動長度欄位]的數量,將"0300"轉為十六進位表示的"0x0003",再轉成以十進位表示的"3",指出[可變動長度欄位]的數量為"3",即destination、activity1、activity2這三個欄位

第八個部分:Variable column offset array (範圍: byte 29 ~ byte 34)

當Record中有[可變動長度欄位]時,每個可變動欄位欄位會佔用2 bytes來記錄該欄位值存放位置偏移量,所以共會耗用掉6 bytes(3筆資料*2 bytes),以上圖為例,第一個可變動欄位使用了byte 29 & 30,第二個可變動欄位使用了byte 31 & 32,第三個可變動欄位使用了byte 33 & 34。

將"2900"轉為十六進位表示的"0x0029",再轉成以十進位表示的"41",表示從byte 35 ~ byte 40為第一個可變動欄位儲存資料的區段;將"3200"轉為十六進位表示的"0x0032",再轉成以十進位表示的"50",表示從byte 41 ~ byte 49為第二個可變動欄位儲存資料的區段;將"3f00"轉為十六進位表示的"0x003f",再轉成以十進位表示的"63",表示從byte 50 ~ byte 62為第三個可變動欄位儲存資料的區段

第九個部分:Variable Column Data (範圍: byte 35 ~ byte 62)

其實在前一個部分,已經說明了每個可變動欄位資料的存放區段,這裡我們利用一個工具"ASCII 在線轉換器"來協助驗證,以第一個可變動欄位為例,將十六進位的值"646573743031",轉成ASCII後得到字串"dest01",確實與實際輸入的資料相符,其餘依此類推。

第十個部分:Optional Version Tag/Pointer

在特定情形下才會在Record的後面附加Version Information,在此暫不深入討論。

 

[補充說明]: 何謂Row Overhead?

在第四及第九部分是分別儲存[固定長度欄位]及[可變動長度欄位]的資料外,其餘的部分可視為Row Overhead是為了記錄、描述record其他資訊所必須要額外使用到的空間且此Row Overhead所使用的空間並不是固定的喔,如第六部分(NULL bit map)及第八部分(Variable column offset array),皆會視實際資料的欄位數量及欄位值,來增減所使用的Bytes數量。讓我們來驗證一下:

(1)第一筆Record長度為 63 bytes (見下圖),實際資料(固定+可變動)佔用空間為 6+9+13+4+8+8 = 48 bytes,所以推估Row Overhead應該為 63-48 = 15 bytes

(2)驗證一下,除第四及第九部分外,請將其他部分所佔用的bytes數量相加得到 1+1+2+2+1+2+6 = 15 bytes,與(1)所推估的Row Overhead相同

 

 

Jay Huang