在這篇文章里,我想詳細介紹下SQL Server里唯一與非唯一非聚集索引的區別。看這個文章前,希望你已經理解了聚集和非聚集索引的概念,還有在SQL Server里是如何使用的。
很多人對唯一和非唯一索引非聚集索引的認識都不是很清晰。事實上,SQL Server在存儲上這2類索引有着本質的區別,這些區別會影響到索引占用空間的大小和索引的使用效率。
今天我們從SQL Server里的堆表(Heap table) ,它是沒有聚集索引定義的表,在它建立唯一和非唯一非聚集索引,來開始我們的分析。下列腳本會創建我們的測試表,並插入80000條記錄。每條記錄需要400 bytes,因此SQL Server在每頁可以放20條記錄。這就是說我們的堆表包括4000個數據頁和1個IAM頁。
1 USE ALLOCATIONDB 2 -- Create a table with 393 length + 7 bytes overhead = 400 bytes 3 -- Therefore 20 records can be stored on one page (8.096 / 400) = 20,24 4 CREATE TABLE CustomersHeap 5 ( 6 CustomerID INT NOT NULL, 7 CustomerName CHAR(100) NOT NULL, 8 CustomerAddress CHAR(100) NOT NULL, 9 Comments CHAR(189) NOT NULL 10 ) 11 GO 12 13 -- Insert 80.000 records 14 DECLARE @i INT = 1 15 WHILE (@i <= 80000) 16 BEGIN 17 INSERT INTO CustomersHeap VALUES 18 ( 19 @i, 20 'CustomerName' + CAST(@i AS CHAR), 21 'CustomerAddress' + CAST(@i AS CHAR), 22 'Comments' + CAST(@i AS CHAR) 23 ) 24 SET @i += 1 25 END 26 GO 27 28 -- Retrieve physical information about the heap table 29 SELECT * FROM sys.dm_db_index_physical_stats 30 ( 31 DB_ID('ALLOCATIONDB'), 32 OBJECT_ID('CustomersHeap'), 33 NULL, 34 NULL, 35 'DETAILED' 36 ) 37 GO
在堆表創建和數據插入后,你就可以在我們的堆表上CustomerID列定義唯一和非唯一非聚集索引。我們把2個索引都定義在同列,這樣我們就可以分析唯一和非唯一聚集索引的區別。
1 -- Create a unique non clustered index 2 CREATE UNIQUE NONCLUSTERED INDEX IDX_UniqueNCI_CustomerID 3 ON CustomersHeap(CustomerID) 4 GO 5 6 -- Create a non-unique non clustered index 7 CREATE NONCLUSTERED INDEX IDX_NonUniqueNCI_CustomerID 8 ON CustomersHeap(CustomerID) 9 GO
如果在非唯一數據的列上定義唯一非聚集索引,SQL Server會返回你一個錯誤信息。當你創建非聚集索引時,如果不指定UNIQUE,SQL Server會創建非唯一的非聚集索引,這點很重要!因此你創建的非聚集索引默認情況下都是非唯一的非聚集索引。
在2個索引創建后,我們可以分析它們的大小,索引深度,索引大小等。使用DMV sys.dm_db_index_physical_stats,第3個參數傳入index-id值。所有非聚集索引的ID值都開始於2,因此第1個非聚集索引的ID值為2,第2個非聚集索引的ID值為3。
1 -- Retrieve physical information about the unique non-clustered index 2 SELECT * FROM sys.dm_db_index_physical_stats 3 ( 4 DB_ID('ALLOCATIONDB'), 5 OBJECT_ID('CustomersHeap'), 6 2, 7 NULL, 8 'DETAILED' 9 ) 10 GO 11 12 -- Retrieve physical information about the non-unique non-clustered index 13 SELECT * FROM sys.dm_db_index_physical_stats 14 ( 15 DB_ID('ALLOCATIONDB'), 16 OBJECT_ID('CustomersHeap'), 17 3, 18 NULL, 19 'DETAILED' 20 ) 21 GO
從輸出結果你可以看到,唯一非聚集索引的索引根頁占用約24%,非唯一非聚集索引的索引根頁占用約39%,因此在堆表上,唯一/非唯一非聚集索引的存儲格式肯定不一樣!下一步我們用一個幫助表來存儲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 TINYINT, 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 25 EXEC('DBCC IND(ALLOCATIONDB, CustomersHeap, 2)') 26 GO 27 28 -- Write everything in a table for further analysis 29 INSERT INTO sp_table_pages 30 EXEC('DBCC IND(ALLOCATIONDB, CustomersHeap, 3)') 31 GO
現在我們可以用DBCC PAGE命令分下聚集索引頁,使用這個命令前我們需要運行 DBCC TRACEON(3604)。在此之前,我們先找下根頁。
1 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC,IndexID
可以看到,唯一非聚集索引的根頁是14624;非唯一非聚集的根頁是14608。
1 DBCC TRACEON(3604) 2 GO 3 DBCC PAGE(ALLOCATIONDB, 1, 14624, 3) 4 GO
從輸出結果我們可以看到,SQL Server存儲者B樹的子頁信息,即非聚集索引最小鍵值位置。例如,14537頁包含最小鍵值540到1078值的記錄。當你使用參數1的DBCC PAGE時,你就獲得了索引根頁上,所有索引記錄字節顯示內容:
1 DBCC TRACEON(3604) 2 GO 3 DBCC PAGE(ALLOCATIONDB, 1, 14624, 1) 4 GO
SQL Server這里需要11個字節來存儲索引行,這些11個字節存儲下列信息:
- 1 byte:狀態位
- 4 bytes:索引鍵值(CustomerID),例如 540
- 4 bytes:子頁ID值(ChildPageId),例如 14537
- 2 bytes: 字段ID值(FileId),例如 1
索引行的長度取決於非聚集鍵的長度。這就是說如果你選擇更短的非聚集鍵,SQL Server就可以保存更多的索引行。如果你選擇了CHAR(100)類型字段作為非聚集索引鍵,SQL Server就需要更多的索引頁來保存你的非聚集索引,因此使用長度短的索引鍵更高效。
最后我們看看子頁14537的內容:
1 DBCC TRACEON(3604) 2 GO 3 DBCC PAGE(ALLOCATIONDB, 1, 14537, 3) 4 GO
從圖中,我們可以看到,SQL Server保存了數據頁的索引鍵(CustomerID (key))和用於定位對應記錄的槽號(HEAP RID)。因為我們在表上沒有定義聚集索引,SQL Server這里使用RID來指向數據頁的記錄。在堆表上的葉子層的索引頁和聚集表上葉子層的索引頁是不一樣的。如果你用參數1來使用DBCC PAGE時,你就得到如下顯示:
1 DBCC TRACEON(3604) 2 GO 3 DBCC PAGE(ALLOCATIONDB, 1, 14537, 1) 4 GO
SQL Server需要13字節來保存每個索引行:
- 1 byte 狀態位
- 4 bytes 索引鍵ID(CustomerID),例如540
- 4 bytes 頁ID(PageID)
- 2 bytes 文件ID(FileID)
- 2 bytes 槽號(Slot number)
手頭有了這些信息,我們就很容易定位頁上的記錄,因為知道了頁號,文件號,還有槽號,頁上的記錄就可以很容易定位到。
我們再來看看非唯一的非聚集索引。根頁號是14608,index id是3。我們來看下14608頁的內容。
1 DBCC TRACEON(3604) 2 GO 3 DBCC PAGE(ALLOCATIONDB, 1, 14608, 3) 4 GO
我們看到了不同的東西!!用DBCC PAGE輸出非唯一非聚集索引的根頁內容是不一樣的。這里SQL Server額外增加了“ HEAP RID(key) ”列。這列的值是為了讓你的非唯一非聚集索引唯一。在你索引行里HEAP RID列使用8個額外字節來存儲下列信息,用來保證堆表索引鍵的唯一:
- 4 bytes: 頁號(PageID)
- 2 bytes:文件號(FileID)
- 2 bytes:槽號(Slot number)
在堆表上非唯一非聚集索引上,所有索引層的每個索引行都會增加8個額外字節占用,不包括葉子層,因為葉子層都會保存HEAP RID。因此在你創建非唯一非聚集索引時,請記住索引行的8字節的額外占用。因為我已經說過,默認創建的非聚集索引都是非唯一的。
這個例子,我們的非唯一索引非聚集索引占用空間是唯一非聚集索引的2倍,因為唯一索引需要11 bytes,而非唯一索引需要19 bytes(包括8 bytes的HEAP RID)。我們回頭看下DMV sys.dm_db_index_physical_stats的信息輸出,唯一非聚集索引的根頁,頁面空間使用率約24%,而非唯一非聚集索引的根頁,頁面空間使用率是39%。在大的非聚集索引上會更加明顯。
因此當你用下列腳本定義非聚集索引時:
1 CREATE NONCLUSTERED INDEX ...
如果不考慮下你數據的唯一性,你的非聚集索引就在浪費大量的存儲空間,降低你的索引性能,並增加日后索引維護難度。
這個系列的下篇文章我們會看下唯一和非唯一非聚集索引之間的區別,請繼續關注!