在SQL Server里,有2種表是以存儲為基礎的。有聚集索引的表叫聚集表,沒有聚集索引的表叫堆表。在上一篇文章,我們討論了堆表的特性和存儲結構。在這篇文章里,我們來看下聚集表。
有聚集索引的表叫聚集表。聚集索引保存了使用B樹結構的聚集鍵,並只能以此順序存儲實際的數據。這也是SQL Server限制一個表只能有一個聚集索引,因為物理存儲順序只能有一個。我們來看看B樹結構的邏輯呈現。下圖是基於AdventureWorks2008R2數據庫,表SalesOrderDetail創建的。
1 USE IndexDB 2 GO 3 SELECT * INTO dbo.SalesOrderDetail FROM AdventureWorks2008R2.Sales.SalesOrderDetail 4 GO 5 CREATE UNIQUE CLUSTERED INDEX ix_SalesOrderDetail ON dbo.SalesOrderDetail(SalesOrderDetailID)
創建一個幫助表,並通過DBCC IND將表信息導入方便進一步分析。
1 -- Create a helper table 2 CREATE TABLE sp_table_pages 3 ( 4 PageFID TINYINT, 5 PagePID INT, 6 IAMFID TINYINT, 7 IAMPID INT, 8 ObjectID INT, 9 IndexID TINYINT, 10 PartitionNumber TINYINT, 11 PartitionID BIGINT, 12 iam_chain_type VARCHAR(30), 13 PageType TINYINT, 14 IndexLevel TINYINT, 15 NextPageFID TINYINT, 16 NextPagePID INT, 17 PrevPageFID INT, 18 PrevPagePID int, 19 PRIMARY KEY (PageFID, PagePID) 20 ) 21 GO 22 23 -- Write everything in a table for further analysis 24 INSERT INTO sp_table_pages EXEC('DBCC IND(IndexDB,SalesOrderDetail,-1)') 25 GO
提取跟節點/索引頁,中間級/索引頁,葉子節點/數據頁信息。
1 SELECT * FROM dbo.sp_table_pages WHERE IndexLevel=2 --根節點/索引頁 2 DBCC TRACEON(3604) 3 DBCC PAGE(IndexDB,1,90,3) 4 5 SELECT * FROM dbo.sp_table_pages WHERE IndexLevel=1 --中間級/索引頁 6 DBCC TRACEON(3604) 7 DBCC PAGE(IndexDB,1,1864,3) 8 DBCC TRACEON(3604) 9 DBCC PAGE(IndexDB,1,1832,3) 10 DBCC TRACEON(3604) 11 DBCC PAGE(IndexDB,1,1808,3) 12 DBCC TRACEON(3604) 13 DBCC PAGE(IndexDB,1,1896,3) 14 15 SELECT * FROM dbo.sp_table_pages WHERE IndexLevel=0 --葉子節點/數據頁 16 DBCC TRACEON(3604) 17 DBCC PAGE(IndexDB,1,1704,3) 18 DBCC TRACEON(3604) 19 DBCC PAGE(IndexDB,1,1720,3) 20 DBCC TRACEON(3604) 21 DBCC PAGE(IndexDB,1,1752,3) 22 DBCC TRACEON(3604) 23 DBCC PAGE(IndexDB,1,1784,3)
根據上述信息進行聚集索引結構示例圖繪制。

這張表有121317條記錄。SQL Server需要3層來存儲這個數據。我們根據上圖來分析下頁。在最高層,你可以看到只有一個頁,這個叫做根頁(root page)。在所有的B樹結構里,都只有一個根頁作為樹結構的訪問入口點。根層始終是最高層。在我們實例里根頁有第2層索引。在根層(root level)和中間級別(intermediate level)的頁叫索引頁。在索引頁里,SQL Server保存着聚集鍵(clustering key)和B樹下層的入口點(頁面指針)。聚集鍵保存的子頁id,最小值保存在下層頁(子頁)。在指定子頁上的聚集鍵最大值可以通過下一記錄找到。例如,在根層第一條記錄(Salesorderdetailid =NULL,pageid=1864),Salesorderdetailid小於等於30226可以在1864號頁找到。入口(Salesorderdetailid =30226,pageid=1832))表示,salesorderdetailid值在30226與60003之間的記錄可以在1832號頁找到,以此類推。在那層,上一頁和下一頁的值將做雙向鏈接來連接這些頁。在根層因為只有一個頁,所以上一頁和下一頁的值為0。
我們移到下一層來看,在這層有4個頁。你可以在下一頁和上一頁里找到值,也是用來鏈接那層的頁。這層被稱為中間層(intermediate level)。中間層的個數和中間層的頁數取決於表的大小和聚集鍵。一個大表可以有多個中間層,小表可能就沒有中間層。這層的值可以和根層一樣的方式讀取。例如,這層的第1頁的第一個入口(Salesorderdetailid =NULL,pageid=1704)表示,salesorderdetailid值小於等於72的可以在1704號頁找到。
下一層是底層,稱為葉子層(leaf level,index level 0)。在這層的頁被稱為葉子頁(leaf pages)或數據頁(data pages)。在這些頁里,你可以找到Salesorderdetail表記錄的全部數據(所有列)。換句話說,聚集索引的葉子層是實際數據存放的地方。
我們復制一張沒有聚集索引的Salesorderdetail表。
1 SELECT * INTO dbo.SalesOrderDetailHeap FROM AdventureWorks2008R2.Sales.SalesOrderDetail 2 GO
我們來執行下列查詢,2個查詢都返回同樣的結果,這里我們更關注的是IO部分。
1 SET STATISTICS IO ON 2 GO 3 SELECT * FROM SalesOrderDetail WHERE SalesOrderDetailID =75 4 GO 5 SELECT * FROM SalesOrderDetailheap WHERE SalesOrderDetailID =75
IO統計信息如下顯示。有聚集索引的表,相比另一個表,邏輯讀對它來說可以忽略不計的。
我們來看看SQL如何使用聚集索引的3個邏輯讀取來拿到記錄的。首先我們需要找出聚集索引的根節點(root node),DBCC IND命令可以幫我們找到。
1 DBCC IND('IndexDB','SalesOrderDetail',1)
返回1501條記錄,包含IAM頁,一個索引頁,和1499個數據頁,部分結果顯示如下。

從輸出結果,我們可以知道90頁(page type 2)是根頁,這個頁是這個表的入口點(entry point)。我們用DBCC PAGE看下這個頁。
1 DBCC traceon(3604) 2 GO 3 DBCC page('IndexDB',1,90,3)

SQL Server在子頁保存聚集鍵的最小值,它的頁號索引頁里。例如,1864號頁會有表salesorderdetailid列值小於等於30226的所有記錄。同樣,1832號頁會有表salesorderdetailid列值在30226與60003之間的所有記錄(30226可能在這2個頁都有)。我們查找這條記錄的salesorderdetailid值小於30226,所以這條記錄的所有信息可以在子頁1864找到。
我們用DBCC PAGE看下這個1864頁。
1 DBCC traceon(3604) 2 GO 3 DBCC page('IndexDB',1,1864,3)
輸出結果包含410條記錄,下面是部分結果顯示。你可以參數1(最后一個參數)來運行DBCC PAGE命令來看頁頭。那樣我們可以找到m_type=2的索引頁。用我們剛才描述的方法,我們知道要找的記錄(salesorderdetailid=75)可以在子頁1705里找到。

我們看下1704頁的內容:
1 DBCC traceon(3604) 2 GO 3 DBCC page('IndexDB',1,1705,3) with tableresults

從頁頭部分,我們可以看到m_type是1,因此這個頁是數據頁,且是索引的葉子層。我們把如下輸出結果一直往下翻。我們就有記錄所有列的值,就是聚集索引葉子層,即實際的數據。
SQL Server從聚集索引只讀取3頁(根頁,中間層的1頁,還有葉子節點的1頁,即數據頁)就找到了我們的記錄。
我們用DBCC IND命令比較下2個表的區別(SalesOrderDetail,SalesOrderDetailHeap)
1 DBCC IND('IndexDB','SalesOrderDetail',1) 2 DBCC IND('IndexDB','SalesOrderDetailHeap',1)
SalesOrderDetailHeap表是堆表,只有1496個頁;SalesOrderDetail表是聚集表,包含一個聚集索引,卻有1501個頁。這個多出的5頁用來存儲B樹結構的中間級(intermediate)和根級(root)的索引頁。我們當他是聚集索引的優點吧,多用5個頁,卻將邏輯讀減少的只有3次。這個存儲開銷還是很划算的。
