一個表只能有一個聚集索引,數據行以此聚集索引的順序進行存儲,一個表卻能有多個非聚集索引。我們已經討論了聚集索引的結構,這篇我們會看下非聚集索引結構。
非聚集索引的邏輯呈現
簡單來說,非聚集索引是表的子集。當我們定義了一個非聚集索引時,SQL Server把整套非聚集索引鍵存在不同的頁里。我們來看下一個包含BusinessEntityID(PK),PersonType,FirstName,LastName這4列的表,這個表上有一個非聚集索引定義。主體表按BusinessEntityID列(聚集索引鍵)的順序存儲。非聚集索引的存儲是與主體表分離的。如果你仔細看非聚集索引表,你會發現,記錄是按Firstname,lastname 列的順序排列的。簡單理解下,非聚集索引就是主體表的子集。
假設現在我們要找出first name值為Michael的記錄。如果你從實體表找的話,我們需要從頭到腳把每條記錄匹配一次,因為記錄並沒有按first name列排序保存。如果這個表記錄有上千條記錄的話,這將是一個非常無聊且費時的工作。如果我們在非聚集索引表里找將會容易很多,因為這個表是按first name列以字母順序排列的。我們很容易定位到first name是Michael的記錄。我們並不需要再往下找,因為我們確定沒有更多的first name是Michael的記錄了。
現在我們得到了Firstname,lastname的值。那我們如何拿到其它2列的值?讓我們對非聚集索引做一些改動,將BusinessEntityID列也作為非聚集索引。
現在,一旦我們定位到記錄,我們可以使用BusinessEntityID(聚集索引鍵)列返回主體表,得到其他列的值,這個操作被稱為書簽查找(bookmark lookups)或RID查找。
聚集索引與非聚集索引
非聚集索引和聚集索引有一樣的B樹結構。非聚集索引鍵不會對主體表的數據排序做任何改變,因為聚集索引強制SQL Server將數據以聚集索引鍵的順序存儲。聚集索引的葉子層由包含表具體數據的數據頁組成,而非聚集索引的葉子層由索引頁組成。
非聚集索引可以定義在堆表或聚集表。在非聚集索引的葉子層,每個索引行包含非聚集索引鍵值和行定位器。這個定位器指向聚集索引或堆表的數據行。在非聚集索引行里的行定位器要么指向行,要么指向行聚集索引鍵。如果是堆表,它沒有聚集索引,行定位器是個指向行的指針。這個指針由頁里行的(文件號:頁號:槽號,file identifier :page number :slot number)組成。整個指針被稱為ROW ID(RID)。如果表有聚集索引,行定位器是行的聚集索引鍵。
非聚集索引深入淺出
我們用文章“索引深入淺出:聚集索引的B樹結構”用到的salesorderdetails創建一個非聚集索引,這個表在salesorderdetailid列有一個聚集索引。
1 CREATE UNIQUE INDEX Ix_ProductId ON SalesOrderDetail(ProductId,Salesorderid)
收集非聚集索引相關信息:
1 TRUNCATE TABLE dbo.sp_table_pages 2 INSERT INTO sp_table_pages EXEC('DBCC IND(IndexDB,SalesOrderDetail,2)') 3 GO 4 5 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC --根節點/索引頁 6 DBCC TRACEON(3604) 7 DBCC PAGE(IndexDB,1,3472,3) 8 9 DBCC TRACEON(3604) 10 DBCC PAGE(IndexDB,1,3416,3)--葉子節點/索引頁 11 12 DBCC TRACEON(3604) 13 DBCC PAGE(IndexDB,1,3557,3)--葉子節點/索引頁 14 SELECT * FROM dbo.sp_table_pages WHERE IndexLevel=0 --葉子節點/索引頁
根據上述信息進行非聚集索引邏輯示意圖的繪制:
現在我們來分析下SQL Server如何存儲非聚集索引,首先我們通過DBCC IND命令查看非聚集索引的頁分配情況,最后一個參數,2是Ix_ProductId的索引號。
1 DBCC IND(IndexDB,SalesOrderDetail,2)
我們看到輸出結果一共有229條記錄,包含1個IAM頁和229個索引頁。我們可以通過找IndexLevel 列值最大的記錄,來找根頁(root page)。記住索引層級是從葉子層向根層增長的。
1 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC --根節點/索引頁
在這個表里,我們根層(root leve)頁號是3472,index level是1,這就是說,這個非聚集索引的B樹結構只有根層(root level)和葉子層(leaf level),沒有中間層(intermediate level)。我們來看看3472頁。
1 DBCC TRACEON(3604) 2 DBCC PAGE(IndexDB,1,3472,3)
返回結果一共有227條記錄(227個葉子層的索引頁)。部分結果如上所示。這和聚集索引里的根層(root)/中間層(intermediate)的頁結構是一樣的。productid與salesorderid組合的值小於或等於(707,51151)的所有記錄,可以在子頁3416里找到。productid與salesorderid組合的值在(707,51151)與(707,55920)之間的所有記錄,可以在子頁3417里找到,並以此類推。
我們來看看3417頁。
1 DBCC TRACEON(3604) 2 DBCC PAGE(IndexDB,1,3417,3)
一共返回539條記錄,都是product id為707的記錄。這里的索引只用2層,這個是B樹結構的葉子層。你會注意到,這里沒有子頁ID列,但我們有salesorderdetailid列(聚集索引鍵),SQL Server用它來進行鍵或書簽查找操作。
我們來看看,SQL Server如何使用這個索引進行一個SELECT操作。點擊工具欄的顯示包含實際的執行計划。
1 SET STATISTICS IO ON 2 GO 3 SELECT * FROM SalesOrderDetail WHERE productid=707 AND SalesOrderid=51192
可以看到執行計划的鍵查找操作。因為這里where條件剛好完全符合我們非聚集索引定義,SQL Server用這個索引來執行查詢。首先SQL Server讀取B樹結構的根頁。我們的查詢條件組合(707,51192)落在根頁的第二條記錄上,因此SQL Server走到它的子頁(頁號3417)。在這個頁里,我們可以用條件組合(707,51192)定位到具體的記錄上,它的salesorderdetailid值是37793。從這里開始,SQL Server使用salesorderdetailid值進行鍵查找(key look up)操作。從上一個文章知道,但我們進行任何聚集索引鍵查找是,需要執行3個I/O。 因此這里,SQL Server需要執行5個I/O操作(2個在非聚集索引,3個在聚集索引的書簽/鍵查找(bookmark/key lookup),這個和你的結果輸出一致。
為了更好的理解它,我們可以把非聚集索引當作salesorderdetail 表的一個子表(我們把它叫做Saleorderdetail_NC),有productid,salesorderid 和 SalesorderDetailid列,並且 ProductId與salesorderid列組合為聚集索引。上述查詢的結果可以通過以下2個查詢來獲得。
1 SELECT * FROM SalesOrderDetail_nc WHERE productid=707 AND SalesOrderid=51192 2 GO 3 SELECT * FROM SalesOrderDetail WHERE SalesOrderDetailid=37793
我們再來看一個查詢:
1 SELECT * FROM SalesOrderDetail WHERE productid=707
查詢返回3083條記錄,查詢條件與非聚集索引的第一列匹配。但是SQL Server並沒用非聚集索引來執行這個查詢,查詢計划如下所示。
這樣做的原因是,如果使用非聚集索引,就需要為3083條記錄執行書簽查找(key lookup)。這會產生9249個I/O操作(3083*3)。因此,SQL Server使用了聚集索引掃描,它只需要1501(對於聚集索引樹結構需要的頁數)個I/O操作。如果我們做一個小的改動,只要Productid ,SalesOrderDetailid和SalesOrderId列,SQL Server會使用非聚集索引,因為它不需要進行書簽查找(bookmark lookup)操作。非聚集索引的葉子層已經包含這些列了。
1 SELECT productid,salesorderdetailid,salesorderid FROM SalesOrderDetail WHERE productid=707
這篇文章真的有點長,而且我是該死的BING輸入法出錯,導致瀏覽器崩潰,丟失一個晚上3個小時成果,重新寫好的,希望大家看了之后可以透徹理解非聚集索引了,晚安各位!!2015-05-14 00:18:42