在上一篇文章里,我們討論了堆表上唯一/非唯一非聚集索引。在SQL Server里沒有聚集索引定義的叫堆表。當你在堆表上定義了一個聚集索引,你的表數據就會重組按聚集鍵的順序進行物理存儲,因為這個表叫做聚集表。這篇文章里,我想談下唯一和非唯一聚集索引之間的區別,這2類聚集索引對存儲的影響。
看這個文章之前,希望你對聚集索引有個基本的認識,并且知道堆表和聚集表之間的區別,還有當在表上定義了一個聚集索引,表里數據頁是如何組織的(B樹結構)。
我們從唯一聚集索引談起。在SQL Server里你有很多方法去定義唯一聚集索引。第1個最簡單的方法就是列上定義一個主鍵(PRIMARY KEY)約束。SQL Server通過在表上創建那列的唯一聚集索引來施行主鍵(PRIMARY KEY)約束。另外一個方法是通過CREATE CLUSTERED INDEX語句來常見唯一聚集索引——但當你不指定UNIQUE屬性時,SQL Server默認是會為你創建非唯一的聚集索引!下列這段代碼會創建Customers表,這個表結構和上篇文章一樣,但這次我們在CustomerID列創建主鍵(PRIMARY KEY)約束。因此SQL Server會在表上創建唯一聚集索引,在葉子層里,數據頁是按CustomerID列值排序的。
1 -- Create a table with 393 length + 7 bytes overhead = 400 bytes 2 -- Therefore 20 records can be stored on one page (8.096 / 400) = 20,24 3 CREATE TABLE Customers 4 ( 5 CustomerID INT NOT NULL PRIMARY KEY IDENTITY(1, 1), 6 CustomerName CHAR(100) NOT NULL, 7 CustomerAddress CHAR(100) NOT NULL, 8 Comments CHAR(189) NOT NULL 9 )10 GO11 12 -- Insert 80.000 records13 DECLARE @i INT = 114 WHILE (@i <= 80000)15 BEGIN16 INSERT INTO Customers VALUES17 (18 'CustomerName' + CAST(@i AS CHAR),19 'CustomerAddress' + CAST(@i AS CHAR),20 'Comments' + CAST(@i AS CHAR)21 )22 23 SET @i += 124 END25 GO
我們可以通過DBCC IND命令找出索引根頁后(PageType為2,IndexLevel為2,即B樹有3層:根和葉子層,PagePID為15359),就可以使用DBCC PAGE查看根頁的內容。這里我的索引根頁是15359。
1 TRUNCATE TABLE dbo.sp_table_pages2 INSERT INTO dbo.sp_table_pages3 EXEC('DBCC IND(ALLOCATIONDB, Customers, -1)') 4 5 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC
1 DBCC PAGE(ALLOCATIONDB, 1, 15359, 3)2 GO
從上圖里,我們可以看到每個索引記錄包含聚集鍵,在這個例子是CustomerID列的值。
如果你從字節存儲級別分析聚集索引記錄的話,你會發現SQL Server這里使用下列字節信息:
可以看出,聚集鍵的長度直接影響索引記錄的長度。這就是說,你的聚集鍵長度越小,索引頁上就可以存放更多的索引記錄,因此你的聚集索引將更緊湊,查找更快,維護更容易。當你在你的聚集索引繼續往下看時,你會發現所有中間層的索引結構和剛才的描述完全一樣。這2層是沒有任何區別的,除了索引葉子層,因為這層包含你實際邏輯排序的數據頁。
現在我們來看看SQL Server里非唯一聚集索引,看看它們和唯一聚集索引的區別。為了演示這類索引,我重建了Customers表,并通過CREATE CLUSTERED INDEX語句在表上創建了非唯一聚集索引。
1 DROP TABLE dbo.Customers 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 Customers 5 ( 6 CustomerID INT NOT NULL, 7 CustomerName CHAR(100) NOT NULL, 8 CustomerAddress CHAR(100) NOT NULL, 9 Comments CHAR(181) NOT NULL10 )11 GO12 13 -- Create a non unique clustered index14 CREATE CLUSTERED INDEX idx_Customers_CustomerID15 ON Customers(CustomerID)16 GO
最后,我插入80000條記錄,這些記錄的CustomerID列(聚集鍵)不再唯一:
1 -- Insert 80.000 records 2 DECLARE @i INT = 1 3 WHILE (@i <= 20000) 4 BEGIN 5 INSERT INTO Customers VALUES 6 ( 7 @i, 8 'CustomerName' + CAST(@i AS CHAR), 9 'CustomerAddress' + CAST(@i AS CHAR),10 'Comments' + CAST(@i AS CHAR)11 )12 INSERT INTO Customers VALUES13 (14 @i,15 'CustomerName' + CAST(@i AS CHAR),16 'CustomerAddress' + CAST(@i AS CHAR),17 'Comments' + CAST(@i AS CHAR)18 )19 INSERT INTO Customers VALUES20 (21 @i,22 'CustomerName' + CAST(@i AS CHAR),23 'CustomerAddress' + CAST(@i AS CHAR),24 'Comments' + CAST(@i AS CHAR)25 )26 27 INSERT INTO Customers VALUES28 (29 @i,30 'CustomerName' + CAST(@i AS CHAR),31 'CustomerAddress' + CAST(@i AS CHAR),32 'Comments' + CAST(@i AS CHAR)33 )34 35 SET @i += 136 END37 GO
我們找下這個非唯一聚集索引的根頁:
1 TRUNCATE TABLE dbo.sp_table_pages2 INSERT INTO dbo.sp_table_pages3 EXEC('DBCC IND(ALLOCATIONDB, Customers, -1)') 4 5 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC
我們再來看看根頁的內容:
1 DBCC PAGE(ALLOCATIONDB, 1, 15359, 3)2 GO
我們發現,SQL Server這里增加了UNIQUIFIER (key)的額外列。這列是SQL Server用來保證非唯一聚集鍵唯一。UNIQUIFIER (key)是4 bytes始于0的長整型值。當你有2條CustomerID值都是1380時,第1條的UNIQUIFIER為0,第2條的UNIQUIFIER值為1。但SQL Server只在索引的導航結構(高于葉子層的所有層)里保存UNIQUIFIER,即葉子層的UNIQUIFIER不為0。SQL Server只在非唯一聚集索引的導航結構里包含0值的UNIQUIFIER,這就是說導航結構里是不物理保存UNIQUIFIER的。在非唯一聚集索引里,唯一保存UNIQUIFIER的地方是在數據頁,就是保存實際數據的地方。下圖是我們聚集聚集索引里的中間層,你會看到UNIQUIFIER在這里是保存的。
1 DBCC PAGE(ALLOCATIONDB, 1, 15359, 3)2 GO3 4 DBCC PAGE(ALLOCATIONDB, 1, 14635, 3)5 GO
最后我們看看數據頁14633:
1 DBCC TRACEON(3604)2 DBCC PAGE(ALLOCATIONDB, 1, 14633, 3) with tableresults3 GO
我們來找4條CustomerID值為1的記錄,看看UNIQUIFIER的值是多少(應該是0,1,2,3)。
因此唯一和非唯一聚集索引的區別是在數據頁,因為當使用非唯一聚集索引時,SQL Server使用4 bytes長的UNIQUIFIER來保證它們唯一,要記住,在你定義非唯一聚集索引時,這個額外開銷始終存在。
下面文章我們會詳細分析下唯一聚集索引上,唯一和非唯一非聚集索引的區別。請繼續關注!
新聞熱點
疑難解答