SQL Server索引管理——索引創建建議和經驗(二)

SQL Server 索引管理——索引創建建議和經驗(二)

索引類型

前文闡述了創建索引要注意的索引寬度、索引順序、索引字段的唯一值比例、索引字段的數據類型選擇等,本文將重點說明索引類型的選擇問題。

SQL Server 2012之前主要的索引爲行索引,即我們常見的聚集索引和非聚集索引,SQL Server 2012及以後,增加列索引,包括聚集列存儲索引和非聚集列存儲索引。列索引主要使用在數據倉庫中,不在本文考慮範圍之內。下面我們主要考慮的行索引的聚集索引和非聚集索引。

聚集索引和非聚集索引都是B-tree結構。兩者的主要不同之處在於聚集索引的葉頁是表的數據頁,和它們指向的數據順序相同。這意味着聚集索引實質就是表。隨着研究的深入,當你決定使用何種類型的索引時,你將會發現兩種索引葉級別的不同變得非常重要。

聚集索引

聚集索引的葉頁和聚集索引所在表的數據頁是相同的。因爲這樣,錶行按照聚集索引列物理排序,並且因爲表數據只能有一個物理順序,一個表僅僅只能有一個聚集索引。

注意:當你創建一個主鍵約束時,如果表上還沒有聚集索引,或者沒有顯示的指定索引爲唯一的非聚集索引時,SQL Server 在主鍵上自動創建一個唯一的聚集索引。這個是不需要的,僅僅是默認行爲,你可以在創建表之前改變它。

堆表

如前文所述,一個沒有聚集索引的表被稱爲堆表。堆表的數據行不是按照任何特定順序存儲的,或者鏈接表中的臨近頁。相比於大的非堆表(有聚集索引的表),堆表的這種無組織的結構通常會增加大的堆表的讀取開銷。

堆表dbcc ind()查詢結果

創建非聚集索引後dbcc ind()堆表查詢結果

使用dbcc page 查看非聚集索引節點9328頁數據情況如下:

從上面非聚集索引頁的查詢結果可以看到,非聚集索引的鍵(Num)指向數據頁的指針HEAP RID,HEAP RID 組成爲28240000:頁編號;0300 文件編號,0000 slot編號

HEAP RID(key):00002428=2*16^3+4*16^2+2*16+8=9256,非聚集索引中的兩條記錄均指向數據頁9256;0000表示第一條數據存儲在9256頁第0個slot,0100表示第二條數據存儲在9256頁第一個slot;0300表示文件編號;

將HEAP RID 轉換爲 文件編號:頁編號:槽編號的腳本如下:

DECLARE @HeapRid BINARY(8)
SET @HeapRid = 0x2824000003000000
SELECT
    
       CONVERT (VARCHAR(5),
                    CONVERT(INT, SUBSTRING(@HeapRid, 6, 1)
                               + SUBSTRING(@HeapRid, 5, 1)))
     + ':'
     + CONVERT(VARCHAR(10),
                    CONVERT(INT, SUBSTRING(@HeapRid, 4, 1)
                               + SUBSTRING(@HeapRid, 3, 1)
                               + SUBSTRING(@HeapRid, 2, 1)
                               + SUBSTRING(@HeapRid, 1, 1)))
     + ':'
          + CONVERT(VARCHAR(5),
                    CONVERT(INT, SUBSTRING(@HeapRid, 8, 1)
                               + SUBSTRING(@HeapRid, 7, 1)))
                               AS 'Fileid:Pageid:slot'

先創建非聚集索引,再創建聚集索引後發現,原數據頁變爲聚集索引頁,同時會發現無論原堆頁的編號(Page PID)還是非聚集索引頁的編號(Page PID)均發生了變化。這是爲何一個表最先需要考慮創建聚集索引的原因(創建聚集索引時,索引索引都會重新創建)。

創建聚集索引後,再查看索引頁,發現,索引頁指向數據指針變爲聚集鍵(ID(key))

在聚集索引ID值1、3中間插入2

INSERT INTO test VALUES(2,3,'c');

可以看到插入新數據後,數據的順序和數據頁的物理順序不一致,從而產生了索引碎片。

沒有聚集索引的表,非聚集索引的HEAP RID可以直接定位數據所在的文件、頁、槽;當在表上創建聚集索引後,非聚集索引是通過聚集索引鍵間接定位到數據所在的文件、頁

聚集索引纔會導致數據頁拆分,導致數據頁的物理順序和邏輯順序不一致,即產生內部索引碎片;堆表不會改變數據頁的邏輯順序;非聚集索引頁的拆分,會產生非聚集索引頁的索引碎片

聚集索引的建議

  • 首先考慮建聚集索引

因爲所有非聚集索引在索引行中均包含聚集索引鍵,聚集索引和非聚集索引的創建順序很重要。例如,如果非聚集索引先於聚集索引創建,那麼非聚集索引的行定位將包含對應表的RID指針。然後創建聚集索引,將修改所有非聚集索引,包含聚集索引鍵,作爲其新的定位值。這將引起所有非聚集索引重建。

考慮到最優性能,我建議在創建任何非聚集索引前,先創建聚集索引。這對最終的性能將沒有影響,但是創建索引本身可能就需要大量的工作。

  • 保持窄索引

因爲所有非聚集索引都以聚集索引鍵作爲其行定位符,爲了獲得最好的性能,保持聚集索引的鍵的字節數儘可能小。例如,如果你創建一個寬的聚集索引,如CHAR(500),這將使得每個非聚集索引增加500字節。因此,保證聚集索引的鍵列儘可能少,仔細考慮聚集索引包含的每一列的大小。INTEGER類型數據通常是聚集索引的較優候選列,而字符數據類型列是次優選擇。

大的聚集索引鍵列,不僅影響其本身的寬度,也加寬了表上所有非聚集索引。這增加了表上所有索引的頁數,增加了邏輯讀和硬盤I/O的需求。

  • 在一個步驟中重建聚集索引

因爲非聚集索引和聚集索引的依賴關係,使用DROP INDEX 和CREATE INDEX語句重建聚集索引,將造成所有非聚集索引重建兩次。爲避免這種情形,使用CREATE INDEX的DROP_EXISTING語句在同一個步驟中重建聚集索引。同樣也可以在重建非聚集索引時使用DROP_EXISTING 語句。

什麼時候使用聚集索引

在特定情形下,使用聚集索引很有幫助。我將在下面的段落中討論這些使用聚集索引的情景。

  • 範圍檢索

因爲聚集索引的葉頁和表的數據頁相同,聚集索引列的順序不僅對聚集索引進行排序,同時也對數據行的物理順序進行了排序。如果數據行的物理順序和請求的數據順序一致,那麼磁頭可以順序讀取所有數據行,不需要太多的磁頭移動。例如,如果一個查詢請求屬於數據庫組中的所有僱員記錄,並且Employees表在Group列有一個聚集索引,則索引相關僱員的行將會在磁盤上被物理的分配在一起。這運行磁頭從第一行位置開始移動,然後用最少的物理移動磁頭,以電子方式順序讀取所有數據。反之,如果數據行不是以正確的物理方式存儲在磁盤上,磁頭必須隨機的從一個位置移動到另一個位置,獲取相關的數據行。因爲磁頭的物理移動佔用了磁盤操作大部分消耗,以合適的物理順序存儲在磁盤上(使用聚集索引),有利於優化I/O消耗。

一個範圍的數據是在關係型系統中頻繁的讀取另外一個表的外鍵。這個數據依賴於應用的讀取機制,是聚集索引很好的候選者。

  • 檢索預排序數據

當檢索的數據需要排序時,使用聚集索引特別有效。如果你在一個或幾個需要排序的列上創建聚集索引,則數據行 物理存儲會按照那個順序,減少了數據檢索後排序的開銷。

下面我們用一個實例來說明聚集索引對範圍查找、排序查詢的影響

IF (SELECT OBJECT_ID('od',N'U')) IS NOT NULL
       DROP TABLE od;
GO
SELECT *
INTO dbo.od
FROM Purchasing.PurchaseOrders;

沒有建聚集索引之前的範圍查找od.PurchaseOrderID BETWEEN 500 AND 510

SET STATISTICS IO ON;
SELECT * FROM dbo.od
WHERE od.PurchaseOrderID BETWEEN 500 AND 510

沒有建聚集索引之前的排序查找ORDER BY PurchaseOrderID DESC;

SELECT * FROM dbo.od
WHERE od.PurchaseOrderID BETWEEN 500 AND 510
ORDER BY PurchaseOrderID DESC;

CREATE CLUSTERED INDEX od_cl_POID ON od(PurchaseOrderID);

創建聚集索引後的範圍查找:

創建聚集索引後的排序查找

比較創建聚集索引後的邏輯讀均顯著下降,並且創建聚集索引的排序查找,不需要再使用臨時表(worktable)進行額外的排序。

現在把聚集索引刪除,創建非聚集索引

DROP INDEX od_cl_POID ON dbo.od;
CREATE NONCLUSTERED INDEX od_ix_POID ON od(PurchaseOrderID);

並運行對應的查詢,結果如下:

比較聚集索引、非聚集索引的邏輯讀的情況,很明顯,聚集索引下的範圍查找、排序查找的性能都顯著優於非聚集索引下的範圍查找、排序查找 

什麼時候不使用聚集索引

在某些特定的情形下,最好不要使用聚集索引,我將在下面討論這些情形。

  • 頻繁更新的列

如果聚集索引列頻繁更新,這將導致所有非聚集索引的行定位符隨之更新,顯著增加了相關語句的開銷。這也會通過阻塞引用表和那個期間的非聚集索引相同內容而影響數據庫的併發。

爲了理解UPDATE語句的消耗,通過對比表上的非聚集索引,獲得更新的性能增加因表中聚集索引鍵列而增加的。考慮如下例子。Sales.SpecialOfferProduct表在主鍵上有一個聚集索引,其也是兩個不同表的外鍵;這是一個典型的many-to-many聯查。在這個例子中,我使用下面的語句,更新聚集索引鍵列中的一列(注意,使用事務,保證測試數據完整)。

USE AdventureWorks2016CTP3
BEGIN TRANSACTION;
SET STATISTICS IO ON;
UPDATE Sales.SpecialOfferProduct
SET ProductId=345
WHERE SpecialOfferID=1
       AND productid=720;
SET STATISTICS IO OFF;
ROLLBACK TRANSACTION;

如果你在表上增加一個非聚集索引,你將看到讀在增長

CREATE NONCLUSTERED INDEX ixTest ON Sales.SpecialOfferProduct(ModifiedDate);

正如你所看到的,由更新聚集索引引起的讀的數量因爲其他的非聚集索引而增加。最後刪除測試索引

DROP INDEX ixTest ON Sales.SpecialOfferProduct;
  • 寬列

因爲所有非聚集索引以聚集鍵作爲其行定位符,考慮到性能因素,你應該避免創建寬列聚集索引,或者在多列上創建聚集索引。像前面所描述的,聚集索引應該儘可能的窄。

  • 以序列順序有太多的併發插入

如果你想要併發的增加許多行新數據,那麼,如果將其分佈在表的不同數據頁上,可能會提高性能。然而,如果你按照同一個順序增加列,即按照聚集索引強加的順序,那麼所有的插入將試圖寫入表的最後一個數據頁。這可能造成在對應磁盤扇形區域內出現大量的熱點(hot spot)。爲避免磁盤熱點,你就不應該安排數據行邏輯順序和其物理順序相同。插入可以在整個表中隨機進行,方法是在另一列上創建一個聚集索引,該索引不按與新行相同的順序排列行。這個問題僅僅針對大量併發插入的情形。

這個建議有一個警告。允許在表底部插入,阻止了爲適合新行而導致中間頁的分頁。如果併發插入數量較低,那麼排序數據(使用聚集索引),按照新行的順序將防止中間頁分頁。然而,如果磁盤熱點變爲性能瓶頸,那麼新行可以調整到中間頁,通過減少表填充因子減少頁拆分。另外,熱點頁將會在內存中,這也有利於性能提升。

如果喜歡,可以搜索關注 MSSQLServer 公衆號,將有更多精彩內容分享:

                                                                 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章