從效能的角度談SQL Server聚集索引鍵的選擇

宋沄劍發表於2015-12-21

簡介

在SQL Server中,資料是按頁進行存放的。而為表加上聚集索引後,SQL Server對於資料的查詢就是按照聚集索引的列作為關鍵字進行了。因此對於聚集索引的選擇對效能的影響就變得十分重要了。本文從旨在從效能的角度來談聚集索引的選擇,但這僅僅是從效能方面考慮。對於有特殊業務要求的表,則需要按實際情況進行選擇。

 

聚集索引所在的列或列的組合最好是唯一的

這個原因需要從資料的存放原理來談。在SQL Server中,資料的存放方式並不是以行(Row)為單位,而是以頁為單位。因此,在查詢資料時,SQL Server查詢的最小單位實際上是頁。也就是說即使你只查詢一行很小的資料,SQL Server也會將整個頁查詢出來,放到緩衝池中。

每一個頁的大小是8K。每個頁都會有一個對於SQL Server來說的實體地址。這個地址的寫法是 檔案號:頁號(理解檔案號需要你對檔案和檔案組有所瞭解).比如第一個檔案的第50頁。則頁號為1:50。當表沒有聚集索引時,表中的資料頁是以堆(Heap)進行存放的,在頁的基礎上,SQL Server通過一個額外的行號來唯一確定每一行,這也就是傳說中的RID。RID是檔案號:頁號:行號來進行表示的,假設這一行在前面所說的頁中的第5行,則RID表示為1:50:5,如圖1所示。

sqlraid1

圖1.RID的示例

從RID的概念來看,RID不僅僅是SQL Server唯一確定每一行的依據,也是存放行的存放位置。當頁通過堆(Heap)進行組織時,頁很少進行移動。

而當表上建立聚集索引時,表中的頁按照B樹進行組織。此時,SQL Server尋找行不再是按RID進行查詢,轉而使用了關鍵字,也就是聚集索引的列作為關鍵字進行查詢。假設圖1的表中,我們設定DepartmentID列作為聚集索引列。則B樹的非葉子節點的行中只包含了DepartmentID和指向下一層節點的書籤(BookMark)。

而當我們建立的聚集索引的值不唯一時,SQL Server則無法僅僅通過聚集索引列(也就是關鍵字)唯一確定一行。此時,為了實現對每一行的唯一區分,則需要SQL Server為相同值的聚集索引列生成一個額外的標識資訊進行區分,這也就是所謂的uniquifiers。而使用了uniquifier後,對效能產生的影響分為如下兩部分:

  •     SQL Server必須在插入或者更新時對現在資料進行判斷是否和現有的鍵重複,如果重複,則需要生成uniquifier,這個是一筆額外開銷。
  •     因為需要對相同值的鍵新增額外的uniquifier來區分,因此鍵的大小被額外的增加了。因此無論是葉子節點和非葉子節點,都需要更多的頁進行儲存。從而還影響到了非聚集索引,使得非聚集索引的書籤列變大,從而使得非聚集索引也需要更多的頁進行儲存。

下面我們進行測試,建立一個測試表,建立聚集索引。插入10萬條測試資料,其中每2條一重複,如圖2所示。

sqlraid2

圖2.插入資料的測試程式碼

此時,我們來檢視這個表所佔的頁數,如圖3所示。

sqlraid3

圖3.插入重複鍵後10萬資料佔了359頁

 

我們再次插入10萬不重複的資料,如圖4所示。

sqlraid4

圖4.插入10萬不重複的建的程式碼

 

此時,所佔頁數縮減為335頁,如圖5所示。

sqlraid5

圖5.插入不重複鍵後縮減為335頁

 

因此,推薦聚集索引所在列使用唯一鍵。

 

最好使用窄列或窄列組合作為聚集索引列

這個道理和上面減少頁的原理一樣,窄列使得鍵的大小變小。使得聚集索引的非葉子節點減少,而非聚集索引的書籤變小,從而葉子節點頁變得更少。最終提高了效能。

 

使用值很少變動的列或列的組合作為聚集索引列

在前面我們知道。當為表建立聚集索引後。SQL Server按照鍵查詢行。因為在B數中,資料是有序的,所以當聚集索引鍵發生改變時,不僅僅需要改變值本身,還需要改變這個鍵所在行的位置(RID),因此有可能使得行從一頁移動到另一頁。從而達到有序。因此會帶來如下問題:

  •     行從一頁移動到另一頁,這個操作是需要開銷的,不僅如此,這個操作還可能影響到其他行,使得其他行也需要移動位置,有可能產生分頁
  •     行在頁之間的移動會產生索引碎片
  •     鍵的改變會影響到非聚集索引,使得非聚集索引的書籤也需要改變,這又是一筆額外的開銷

這也就是為什麼很多表建立一列與資料本身無關的列作為主鍵比如AdventureWorks資料庫中的Person.Address表,使用AddressID這個和資料本身無關的列作為聚集索引列,如圖6所示。而使用AddressLine1作為主鍵的話,員工地址的變動則可能造成上面列表的問題。

sqlraid6

圖6.建立和資料本身無關的一列作為聚集索引列

 

最好使用自增列作為聚集索引列

這個建議也同樣推薦建立一個和資料本身無關的自增列作為聚集索引列。我們知道,如果新新增進來的資料如果聚集索引列需要插入當前有序的B樹中,則需要移動其它的行來給新插入的行騰出位置。因此可能會造成分頁和索引碎片。同樣的,還會造成修改非聚集索引的額外負擔。而使用自增列,新行的插入則會大大的減少分頁和碎片。

最近我碰到過一個情況。一個表每隔幾個月效能就奇慢無比,初步檢視是由於有大量的索引碎片。可是每隔幾個月重建一次索引讓我無比厭煩。最終我發現,問題是由於當時設計資料庫的人員將聚集索引建在了GUID上,而GUID是隨機生成的,則可能插入到表的任何位置,從而大大增加了碎片的數量。因此造成上面這種情況。

 

總結

本文簡單介紹了SQL Server儲存的原理和應該規避的幾種聚集索引建立情況,但這僅僅是從效能的角度來談聚集索引的選擇。對於聚集索引的選擇,還是需要全面的考慮進行決定。

相關文章