堆資料表是沒有聚集索引的表。即資料行不按任何特殊的順序存儲,資料頁也沒有任何特殊的順序。資料頁不在連結清單内連結。 sys.system_internals_allocation_units 系統視圖中的列 first_iam_page 指向管理特定分區中堆的配置設定空間的一系列 IAM 頁的第一頁。SQL Server 使用 IAM 頁在堆中移動。堆内的資料頁和行沒有任何特定的順序,也不連結在一起。資料頁之間唯一的邏輯連接配接是記錄在 IAM 頁内的資訊。
那麼堆表是如何存儲資料的呢?出于簡化的目的,我們先來構造不含任何索引的一張堆資料表,然後從簡單到複雜逐漸深入探讨。
--建立一張資料表,從系統表生成測試資料
DROP TABLE testheap
--建立一張2個固定長度字段,3個不定長字段的表,其中4個不為空,1個可為空
CREATE TABLE testheap
(
ID INT IDENTITY(1,1) NOT NULL,
name VARCHAR(20) NOT NULL,
type CHAR(100) NOT NULL,
other VARCHAR(50) NOT NULL,
describle VARCHAR(500)
)
--産生1000條随機資料,并插入表中
DECLARE @i INT
SET @i=1
WHILE @i<=1000
BEGIN
INSERT INTO testheap(name,type,other,describle)
VALUES('name'+CAST(@i AS VARCHAR(3)),REPLICATE(@i%4,100),FLOOR(RAND()*10),NULL)
SET @i=@i+1
END
SELECT * FROM testheap
--查詢該表的IAM頁面位址和首頁位址
SELECT total_pages,used_pages,data_pages,
--first_page,root_page,first_iam_page,
testdb.dbo.f_get_page(first_page) first_page_address,
testdb.dbo.f_get_page(root_page) root_address,
testdb.dbo.f_get_page(first_iam_page) IAM_address
FROM sys.system_internals_allocation_units
WHERE container_id IN (SELECT partition_id FROM sys.partitions
WHERE object_id in (SELECT object_id FROM sys.objects
WHERE name IN ('testheap')))
查詢結果如下:
total_pages
used_pages
data_pages
first_page_address
root_address
IAM_address
25
18
17
1:224
0:0
1:119
即SQL Server為該表配置設定了總計25個頁面,實際使用了18個頁面,扣除1個IAM管理頁面,實際資料頁面為17個,IAM管理頁面位址為第一個檔案的第119頁面,資料頁面的第一個頁面為第一個檔案的第224頁面。
那麼如何檢視到該表的頁面詳細配置設定情況呢?
首先通過dbcc page(testdb,1,119,3)可以粗略看到頁面配置設定情況
即SQL Server首先配置設定了8個混合區頁面,其次因為該對象已經超過8頁,SQL Server又配置設定了從第472頁到第487頁的頁面,共計16個頁面,然後包括本身的IAM頁面,共計25個頁面。
其次SQL Server還提供了一個更為友好的指令以找到各個類型的頁面分布和它們的所在的檔案号和頁号。
DBCC IND({'dbname'|dbid},{'objectname'|objectID},
{nonclustered indid|1|0|-1|-2}[,partition_number])
{'dbname'|dbid}表示資料庫名或者資料庫ID
{'objectname'|objectID}表示對象名或者對象ID
{nonclustered indid|1|0|-1|-2}表示顯示行内資料分頁及指定對象的行内IAM分頁資訊
1 :顯示所有分頁的資訊,包括IAM分頁,資料分頁,所有存在的LOB分頁和行溢出頁,索引分頁
-1: 顯示所有IAM、資料分頁、及指定對象上全部索引的索引分頁.
-2: 顯示指定對象的所有IAM分頁
nonclustered indid:顯示所有的IAM、資料分頁以及一個索引的索引分頁資訊。
{partition_number}->可選,為了與中的DBCC IND指令向前相容.它指定了一個特定分區号,如果不指定,顯示所有分區的資訊。
以下是DBCC IND指令輸出結果的字段描述:
字段名稱
字段描述
PageFID
索引檔案的ID
IAMFID
管理該分頁的IAM分頁所在的檔案ID
管理該分頁的IAM分頁的ID
ObjectID
對象ID
IndexID
索引ID,0 代表堆, 1 代表聚集索引, 2-250 代表非聚集索引
PartitionNumber
表或索引所在的分區号碼
PartitionID
包含該分頁的分區ID
iam_chain_type
該頁所屬配置設定單元類型;行内資料、行溢出資料或Lob資料
PageType
分頁類型:1資料頁面;2索引頁面;3Lob_mixed_page;4Lob_tree_page;10IAM頁面
IndexLevel
索引層級,0 代表葉級别分頁 ;>0 代表非葉級别層次; NULL 代表IAM分頁
NextPageFID
本層下一個分頁所在的檔案ID
本層下一個分頁ID
PrevPageFID
本層上一個分頁所在的檔案ID
本層上一個分頁ID
繼續為了簡化的目的,同時因為模拟的是小型資料表,是以可以忽略相關檔案号和iam鍊類型、分區号(該表暫無行内遷移和lob字段),我們隻需要看看各 個資料頁之間是否有互相聯系、各個頁面的類型即可;是以我們建構了一張資料表用以存放dbcc ind指令輸出的結果,并有選擇性的選擇我們想要的字段。
CREATE TABLE tablepage
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
);
GO
TRUNCATE TABLE tablepage;
INSERT INTO tablepage EXEC ('DBCC IND(testdb,testheap,1)');
SELECT
PagePID,IAMPID,ObjectID,IndexID,Pagetype,IndexLevel,
NextPagePID,PrevPagePID
FROM tablepage
最終結果如下:
我們可以看到SQL Server為該表所使用的頁面位址,索引ID、頁面類型、索引級别、前後頁的關系等等。
Pagetype=10為IAM頁面,Pagetype=1為資料頁面,即17個資料頁面,1個IAM頁面,與 system_internals_allocation_units輸出結果一緻,每一個資料頁面都對應該IAM頁面位址,indexid=0表示為堆 表,indexlevel=null表示為IAM頁面,indexlevel=0表示為葉子節點;而讓我們感到有些失望的是每一個頁面似乎除了有共同的 IAM管理頁面之外,互相之間是缺乏聯系的。
而且從dbcc ind運作的結果來看,每個頁面好像也是不連續的,那麼首先通過Internals Viewer插件讓我們看一下IAM頁的情況吧,前八頁是斷斷續續的分散分布的,而後面的16頁卻是連續的,再回頭看一下tablepage表也印證了這 個現象。既然頁面與頁面之間缺乏聯系,那麼對堆表資料的通路隻能靠IAM頁來管理和定位了。
SQL Server資料頁的結構大體包括三個部分,即标頭、資料行和行偏移量。
現在讓我們正式進入資料頁面去看一下資料頁面的構造,讓我們首先去通路一下該表的資料首頁即第224個頁面。
Dbcc page(testdb,1,224,2)
PAGE HEADER部分,即該頁面的前96個位元組。
m_pageId = (1:224)
目前頁面号碼
m_headerVersion = 1
版本号,始終為1
m_type = 1
目前頁面類型,m_type=1表示資料頁面
m_typeFlagBits = 0x4
資料頁和索引頁為4,其他頁為0
m_level = 0
該頁在索引頁(B樹)中的級數,0表示為葉子節點
m_flagBits = 0x8200
頁面标志
m_objId (AllocUnitId.idObj) = 94
m_indexId (AllocUnitId.idInd) = 256
Metadata: AllocUnitId = 72057594044088320
存儲單元的ID,sys.allocation_units.allocation_unit_id
Metadata: PartitionId = 72057594039107584
資料頁所在的分區号,sys.partitions.partition_id
Metadata: IndexId = 0
對象的索引号,sys.objects.object_id&sys.indexes.index_id
Metadata: ObjectId = 133575514
該頁面所屬的對象的id,sys.objects.object_id
m_prevPage = (0:0)
該資料頁的前一頁面
m_nextPage = (0:0)
該資料頁的後一頁面
pminlen = 108
定長資料所占的位元組數為108個位元組
ID INT IDENTITY(1,1) NOT NULL,
type CHAR(100) NOT NULL,
共計104個位元組,每個定長字段需要2個位元組的管理位元組
m_slotCnt = 62
頁面中的資料的行數,每頁62條記錄
m_freeCnt = 293
頁面中剩餘的空間,還剩293位元組的空間
m_freeData = 7775
從第一個位元組到最後一個位元組的空間位元組數(包括96位元組的檔案頭的長度)
m_reservedCnt = 0
活動事務釋放的位元組數
m_lsn = (67:272:3)
日志記錄号
m_xactReserved = 0
最新加入到m_reservedCnt領域的位元組數
m_xdesId = (0:0)
添加到m_reservedCnt的最近的事務id
m_ghostRecCnt = 0
幻影資料的行數
m_tornBits = 1213019927
頁的校驗位或者被由資料庫頁面保護形式決定分頁保護位取代
上在頁的尾部還有個行偏移矩陣,記錄了每條記錄的起始位置,每條記錄需要2個位元組來記錄該位置,是以62條記錄共計124個維護位元組,加上293個剩餘空間和實際已使用的7775個位元組,剛好8192個位元組,即一頁。
從Offset table和page結構可以知道,第一條記錄從第96個位元組開始。
ID
name
Type
other
describle
1
name1
1111111111111111111111111111111111.
8
NULL
如前文所說,關于資料的存儲從第96個位元組開始
關于資料行的結構我們還可以采用稍微宏觀一些的視角來檢視。
其中狀态A為如下說明:
bit0:版本資訊,在SQL Server 2005/08總是為0
bit1-3: 0=(primary record);1=(forwarded record);2=(forwarding stud);3=(index record);4=(溢出資料);5=(ghost索引記錄);6=(ghost資料記錄)
bit4:表示存在NULL位圖(在資料行裡SQL2005/08總存在NULL位圖)
bit5:表示存在變長列
bit6:未啟用
bit7:表示存在幽靈記錄
本例中30->00110000 它是一個行屬性的位圖 從高位存到地位(右邊第一位是bit0),bit4為1即存在變長列的字段,因為在SQLServer2005/2008中總存在NULL位圖,是以bit5也為1。
狀态位B在SQLServer2005//2008中未啟用,是以為00
記錄定長部分的長度為2個位元組,是所有定長字段的長度之和加4,該處為int類型4個位元組,char(100)為100個位元組,再加上4,是以為108,換算成16進制即6c。
緊跟其後的為定長字段的内容,即ID字段的4個位元組和TYPE字段的100個位元組。
固定長度的字段資料之後,是該表的總字段數,用兩個位元組表示,本表包括5個字段是以為05 00。
NULL位圖:f0->11110000 因為該表隻有列 是以隻需要看後面個,1表示該行的對應列為NULL或者該位圖未使用。本表前4個字段不為空,第5個為空,第6-8未使用。
第一變長列資料終止位置為:7a00->00000000 01111010=122=1+1+2+(4+100)+2+ceiling(5/8)+2+2+2+len(“name1”)
第二變長列資料終止位置:7b00->00000000 01111011=123 實際上就是在前者的基礎上加了第二個變長列的字段長度。
1+1+2+(4+100)+2+ceiling(5/8)+2+2+2+len(“name1”)+len(“8”)
第一列變長列的資料: 6e616d 6531換算成字元即'name1'
第二列變長列的資料:38換算成字元即8
下面讓我們将該記錄的describle字段更新為非空值後,再看看該記錄存儲結構相應的變化。
UPDATE testheap SET describle='abc' WHERE id=1
再次使用dbcc page(testdb,1,224,1)指令
我們不難發現狀态A,狀态B,定長長度、定長内容和字段總數是沒有發生任何變化的。
NULL位圖部分變成了e0即11100000,表示describle字段即第五個字段不為空了
第一個和第二個變長列資料終止位置分别加了2個長度,這是因為當第三個變長列變更為非空後,自動添加了2個位元組的第三個字段的維護字段
第一個變長列資料終止位置從7a00變更為7c00
第二個變長列資料終止位置從7b00變更為7d00
新增加的第三個變長列終止位置為8000
同時在第一、二列變長列的資料後面新增加了616263,即字元串”abc”
還有一個最顯著的差別就是該記錄的偏移位置顯然轉到了尾部,即5F1E的位置;但很奇怪的是該記錄原來的位置上還保留着原值,并沒有删除掉。也就是說對于該記錄而言,應該是先删除,然後又添加了一條新紀錄,隻是把指針指向了新的偏移位址而已。
最後觀察一下記錄是如何删除的
DELETE FROM testheap WHERE ID IN (2,3)
當我們對比一下删除前後兩條記錄的資訊,發現基本上原來的位置上資料沒有發生任何變化,隻是原來的slot1和slot2已經不存在了。即SQL Server認為該資料已經不存在了。
<b> 行溢出頁面</b>
USE TESTDB
CREATE TABLE testOverFlow
ID INT IDENTITY(1,1),
NAME1 VARCHAR(5000),
NAME2 VARCHAR(5000)
INSERT INTO testOverFlow (NAME1,NAME2)
SELECT REPLICATE('A',5000),REPLICATE('AB',2500)
UNION
SELECT REPLICATE('B',5000),REPLICATE('BA',2500)
SELECT * FROM testOverFlow
SELECT type_desc
total_pages,used_pages,data_pages,
FROM sys.system_internals_allocation_units
WHERE container_id IN (SELECT partition_id FROM sys.partitions
WHERE object_id in (SELECT object_id FROM sys.objects
WHERE name IN ('testoverflow')))
DBCC TRACEON(3604)
DBCC PAGE(testdb,1,54242,2) --行内資料
DBCC PAGE(testdb,1,52343,2) --行遷移資料
--同時我們也可以通過dbcc ind擷取所有資料頁面位址,然後進行頁面資訊顯示
INSERT INTO tablepage EXEC ('DBCC IND(testdb,testOverFlow,1)');
在NAME2字段之前和普通的行記錄資訊是一緻的,我們隻從NAME2字段開始就可以了。NAME2字段在NAME1字段之後,儲存了以下内容,即改列的溢出列類型、節點類型、資料庫更新次數、字段長度、指向OVERFLOW頁的指針。
0200
0000
0100
00009d75
8813 0000
77cc0000 0100 0000000
溢出列類型
節點類型
Lob資料更新次數
未知
字段長度
行溢出指針
RowOVerFlow
1973223424
5000
1:52343:0
讓我們再來看一下第52343頁看一下行溢出頁的資料情況,該頁面首先是一個LOB類型的頁面,然後主要包括該字段的長度、關聯ID,和資料行;很顯然行 内資料和溢出行資料的關聯是通過一個行溢出指針和ID進行的;是以對某個資料查詢而言,首先要找到該記錄的資訊,同時如果發生行溢出,還有根據該列的行溢 出指針和關聯ID,才能找到整條記錄。
1個位元組
2個位元組
8個位元組
4個位元組
9613
0300
狀态A
狀态B
unkown
類型
即包含行溢出
5014(同變長字段)
lob資料行
<b> LOB頁面</b>
從SQL Server 2005版本以後中,新增加了大值資料類型varchar(max)、nvarchar(max)、varbinary(max)。大值資料類型最多可以存儲2^30-1個位元組的資料。
從行為上來講這幾個資料類型和之前的資料類型 varchar、nvarchar 和 varbinary 相同。
按照微軟的說法是用這個資料類型來代替之前的text、ntext 和 image 資料類型,它們之間的對應關系為:
varchar(max)-------text;
nvarchar(max)-----ntext;
varbinary(max)----image
對大值資料類型的操作更類似于之前的varchar和varbinary之後,是以用法上也比之前的text和image比靈活和便宜。同時觸發器也可以直接引用大值資料類型;而之前的text和image是不行的。
是以varchar(max)與varchar(n)和text有着千絲萬縷的聯系。對于varbinary(max)也一樣。
因為之前我們已經觀察過varchar(n)的行為,那麼讓我們看看這個新的varchar(max)與varchar(n)、text到底有什麼不同。
CREATE TABLE testVARCHARMAX
ID INT IDENTITY(1,1),
name VARCHAR(20),
remark VARCHAR(MAX)
CREATE TABLE testTEXT
remark TEXT
INSERT INTO testVARCHARMAX (name,remark)
SELECT REPLICATE('A',20),REPLICATE('AB',2500)
SELECT REPLICATE('B',20),REPLICATE('BA',2500)
INSERT INTO testTEXT (name,remark)
SELECT c.name,a.type_desc
FROM sys.system_internals_allocation_units a,sys.partitions b,sys.objects c
WHERE a.container_id=b.partition_id and b.object_id=c.object_id
AND c.name in ('testVARCHARMAX','testTEXT')
運作結果如下:
我們很容易發現兩者的共同之處,就是兩個表都包括LOB_DATA資料類型的配置設定單元,但是testVARCHARMAX表的LOB_DATA并沒有分 配頁面,而testTEXT表卻配置設定了3個頁面;同時testVARCHARMAX表比testTEXT表多了一個資料頁面,這是怎麼回事呢?
讓我們首先看看testVARCHARMAX表的第217個資料頁面
讓我們通過Internals Viewer插件看一下對該記錄的解讀
與之前的堆表的介紹相比,基本上我們可以看到與varchar(n)的存儲結構式完全一緻的,在此就不多做叙述了。
那麼testTEXT表為什麼會使用到LOB類型頁面呢?我們使用dbcc page指令檢視一下。
運作dbcc page(testDB,1,222),我們從第96個位元組開始閱讀。
從這個角度,我們看到222頁面類似于前面所講到的行溢出頁面,即在222頁面保留了一個指向行溢出頁面的指針
運作dbcc page(testDB,1,220,2),我們從第96個位元組開始閱讀。
實際上我們從name字段内容之後閱讀就可以了,即0000d1 07000000 00dc0000 00010001 00
是不是有點像縮略版的行溢出資訊?
既然有行溢出指針,必然有行溢出頁面,那我們再看看行溢出頁面的資料頁,即220頁面。實際上我們用dbcc page(testdb,1,220,3)閱讀該頁的資訊更簡明一些。
很明顯slot 0記錄了第一條記錄remark字段的長度、資料類型和内容。
Slot 1,slot 2分别為兩個指針,記錄了remark字段的偏移位址和相應的檔案号、頁面和槽号
這個與之前的行溢出頁面是有所不同的。
本文轉自baoqiangwang51CTO部落格,原文連結:http://blog.51cto.com/baoqiangwang/414197,如需轉載請自行聯系原作者