翻譯節選《Pro SQL Server Internals, 2nd edition》CHAPTER 2(含圖解)
聚簇索引
文章選自:《Pro SQL Server Internals, 2nd edition》CHAPTER 2
Dmitri Korotkevitch
一個聚簇索引表明表中資料的物理順序,該順序是根據聚簇索引鍵排序的。一個表只能定義一個聚簇索引。
假設你想要在帶有資料的堆表上建立聚簇索引。首先第一步,如圖2-5所示,SQL Server會先建立一個數據副本,然後根據聚簇索引鍵的值進行排序。所有的資料頁都被連線到雙向連結串列中,它們每個頁面都包含指向鏈中的下一個和上一個頁面的指標。這個列表稱為索引的葉級,並且它也包含目前表格的資料
圖2-5 物件:葉級
注意:頁面上的排序順序由槽陣列控制。頁面上的實際資料沒有排序
當葉級包含很多頁面時,SQL Server就會建立一個索引的中間層級,如圖2-6所示。
圖2-6 聚簇索引結構:中間級和葉級
中間級把每個葉級頁面儲存為一行。它儲存了兩條資訊:實體地址和它引用的頁面對應索引鍵的最小值。會出現一種異常是當第一頁只有一行的時候,SQL Server儲存的值是NULL而不是最小索引鍵值。通過這種優化,當你要在最小索引值的表中插一行時,SQL Server就不需要再更新非葉級級別的行。
中間級上的這些頁面也會連結到雙向連結串列中。SQL Server會一直新增中間級,直到有一個級別只包含單個頁面為止。這個層級就叫做根級,作為索引的入口點,如圖2-7所示
圖2-7 聚簇索引結構:根級
就像你看到的一樣,索引總是具有一個葉級、一個根級、零個或多箇中間級。當索引資料輸入單個頁面時會出現一種異常。在這種情況下,SQL Server不會建立單獨的根層級頁面,索引也只包含單個葉級頁面。
索引中的級別數主要取決於行和索引鍵大小。例如,4位元組的整數列上的索引在中間和根層級上每行需要13個位元組。這13個位元組包括一個2位元組的槽陣列條目、一個4位元組的索引鍵值、一個6位元組的頁指標和一個1位元組的行開銷,這已經夠用了,因為索引鍵不包含可變長度和空列。
因此,每頁可以容納8060位元組/ 13位元組=620行。這意味著,每一箇中間層最多可以儲存620 * 620 = 384,400葉級頁的資訊。如果你的資料行大小為200位元組,而且只有三個層級,那麼你的每個葉級頁面可以儲存40行,那你最多可以儲存15,376,000行在索引中。增加另外的中間層級到索引中則基本將覆蓋掉所有的可用整數值。
注意:在現實生活中,索引碎片化將把這些數字分解。我們將在第6章中討論索引碎片化。
SQL Server可以通過三種不同的方式從索引中讀取資料。第一個是有序掃描。假設我們想執行SELECT Name FROM dbo.Customers ORDER BY CustomerId.的查詢。索引頁根層級上的資料就會根據CustomerId列值排序儲存。因此,SQL Server可以從第一個頁面到最後一個頁面掃描索引中的葉級,並按照它們儲存的順序返回行
SQL Server從索引的根頁面開始讀取第一行。該行應用的是來自表的最小鍵值的中間頁面。SQL Server讀取該頁面並重復該過程,直到它找到葉子層級的第一個頁面。然後,SQL Server開始逐個讀取行,遍歷頁面的連結串列,直到讀取了所有行。圖2-8說明了這個過程。
圖2-8 有序索引掃描
前面查詢的執行方案也說明了聚簇索引掃描將其有序屬性設定為true的操作,如圖2-9所示
圖2-9 有序索引掃描執行計劃
值得一提的是,order by子句不需要觸發有序掃描。有序掃描意味著SQL Server根據索引鍵的順序讀取資料。
SQL Server可以全方位查詢索引,包括向前和向後。但是,必須記住一個重要的點:SQL Server在向後索引掃描期間不執行並行性。
提示:你可以通過檢查執行計劃中的索引掃描或索引查詢操作符屬性來檢查掃描方向。但是記住,管理工廠在執行計劃時不會的表示在圖表中顯示這些屬性。你需要開啟屬性視窗,通過在執行計劃中選擇操作符並選擇檢視/屬性視窗選單項或按F4鍵來檢視它。
SQL Server的企業版有一個稱為旋轉木馬掃描的優化特性,它允許多個任務共享相同的索引掃描。假設會話S1掃描索引。在掃描過程中,另一個會話S2也會執行一個查詢,該查詢掃描相同的索引。通過旋轉木馬掃描,S2在當前掃描位置加入S1。SQL Server只讀取每個頁面一次,將行傳遞給兩個會話。
當S1掃描到達索引的末尾時,S2從索引的開頭開始掃描資料,直到S2掃描開始的那一點。旋轉木馬掃描是說明為什麼不能依賴索引鍵的順序,以及為什麼在關鍵的時候應該使用order BY子句的一個例子。
排序掃描之後的下一個訪問方法稱為分配順序掃描。SQL Server通過IAM頁面訪問表資料,這與它通過堆表訪問表資料的方式類似。SELECT Name FROM dbo.Customers WITH (NOLOCK) 這種查詢和圖2-10說明了這種方法。圖2-11說明了查詢執行計劃。
圖2-10 分配順序掃描
圖2-11 分配順序掃描執行計劃
不幸的是,SQL Server在使用分配順序掃描時不容易檢測到。儘管執行計劃中的有序屬性顯示為false,它也表明SQL Server並不在意是否按照索引鍵的順序讀取行,也不關心是否使用了分配順序掃描。分配順序掃描可以更快的掃描大型表,儘管它有較高的啟動成本。當表很小時,SQL Server不使用這種訪問方法。
另一個要重要的考慮因素是資料一致性。SQL Server在聚簇索引的表中不使用轉發指標,分配順序掃描可能產生不同的結果。由於頁面分割導致的資料移動,行可能會被多次跳過或讀取。因此,SQL Server通常不使用分配順序掃描,除非它在未提交或可序列化事務隔離級別讀取資料
注意:我們將在第6章“索引碎片化”中討論頁面分割和碎片,並在第3部分“鎖定、阻塞和併發”中討論鎖定和資料一致性。
最後一種索引訪問方法稱為索引查詢。SELECT Name FROM dbo.Customers WHERE CustomerId BETWEEN 4 AND 7的查詢和圖2-12說明了這個過程。
圖2-12 索引查詢
為了從表中讀取行範圍,SQL Server需要從範圍中找到鍵值最小行,就是4。SQL Server從根頁面開始,第二行引用鍵值最小為350的頁面。它比我們正在找的鍵值(4)要大,SQL Server讀取被根頁引用的中層資料頁(1:170)
類似地,中層級頁面將SQL Server帶到第一個葉級頁面(1:176)。SQL Server讀取該頁面,然後讀取CustomerIds中等於4和5的行,最後剩下的兩行讀取到第二頁。
執行計劃如圖2-13所示。
圖2-13 執行計劃的索引查詢
就像你假設的那樣,索引查詢比索引掃描更有效,因為SQL Server只處理行和資料頁的子集,而不是掃描整個表。
從技術上講,有兩種索引查詢操作。第一個稱為單例查詢,有時也稱為點查詢,意味著SQL Server只查詢和返回一行。你可以想成是操作WHERE CustomerId = 2這樣的例子。另一種型別的索引查詢操作稱為範圍掃描,它要求SQL Server找到鍵的最低或最高值,並去掃描(向前或向後)這些行直到掃描範圍結束。可以認為是WHERE CustomerId BETWEEN 4 AND 7這樣語句導致的範圍掃描。這兩種情況都顯示為執行計劃中的索引查詢操作
正如我們猜測的,範圍掃描可能會強制SQL Server處理來自索引的大量甚至所有資料頁。例如,如果將查詢更改為使用WHERE CustomerId >0,SQL Server將讀取所有行/頁,即使在執行計劃中顯示了索引查詢操作符。你必須記住這種行為,並在查詢效能調優期間始終分析範圍掃描的效率。
關係資料庫中有一個概念叫保留謂詞,它代表搜尋引數的。如果SQL Server可以使用索引查詢操作(如果存在索引),則謂詞是保留。簡而言之,當SQL Server能夠隔離要處理的單個值或索引鍵值範圍時,謂詞是可保留的,從而限制了謂詞計算期間的搜尋。顯然,使用保留謂詞編寫查詢並在任何可能的情況下使用索引搜尋是有益的。
保留謂詞包括以下操作符:=、>、>=、<、<=、IN、BETWEEN和LIKE(在字首匹配的情況下)。非保留操作符包括NOT、<>、LIKE(在非字首匹配的情況下)和NOT in。
使謂詞不可保留的另一種情況是對錶列使用函式或數學計算。SQL Server必須為它處理的每一行呼叫函式或執行計算。幸運的是,在某些情況下,您可以重構查詢,使這些謂詞成為可保留。表2-1顯示了一些例子。
你必須記住的另一個重要因素是型別轉換。在某些情況下,您可以使用不正確的資料型別使謂詞不可保留。讓我們使用varchar列建立一個表,並用一些資料填充它,如列表2-6所示。
列表2 - 6。保留謂詞和資料型別:測試表的建立
聚簇索引鍵列被定義為varchar,儘管它儲存整數值。現在,讓我們執行兩個選擇,如列表2-7所示,並檢視執行計劃。
列表2 - 7。保留謂詞和資料型別:使用整型引數進行選擇
如圖2-14所示,對於integer引數,SQL Server掃描叢集索引,將varchar轉換為每一行的整數。在第二種情況下,SQL Server在開始時將整型引數轉換為varchar,並使用更高效的聚簇索引查詢操作。
圖2 - 14。保留謂詞和資料型別:具有整型引數的執行計劃
提示:注意連線謂詞的列資料型別。隱式或顯式資料型別轉換會顯著降低查詢的效能。
在unicode字串引數的情況下,您將觀察到非常類似的行為。讓我們執行列表2-8所示的查詢。圖2-15顯示了語句的執行計劃。
列表2 - 8。保留謂詞和資料型別:使用字串引數進行選擇
圖2-15。保留謂詞和資料型別:帶有string引數的執行計劃
正向你看到的,對於varchar列,unicode字串引數是不可保留的。這是一個比看上去大得多的問題。雖然很少以這種方式編寫查詢,如清單2-8所示,但是現在大多數應用程式開發環境都將字串視為unicode。結果,SQL Server客戶端庫為字串物件生成unicode (nvarchar)引數,除非引數資料型別明確指定為varchar。這使得謂詞不可保留,由於不必要的掃描在索引varchar列時,就會導致效能下降。
重要:總是在客戶機應用程式指定引數的資料型別。例如,在ADO中,使用Parameters.Add("@ParamName",SqlDbType.Varchar, <Size>).Value = stringVariable代替
Parameters.Add("@ParamName").Value = stringVariable過載。在ORM框架中使用對映指定類中的非unicode屬性。
值得一提的是,對於nvarchar unicode資料列,varchar引數是可保留的。