1. 程式人生 > >《Pro SQL Server Internals, 2nd edition》節選翻譯(三)

《Pro SQL Server Internals, 2nd edition》節選翻譯(三)

第七章 設計和優化索引

聚集索引設計注意事項

  在你改變聚集索引鍵的值時,將會發生兩件事。首先,SQL server移動行到聚集索引頁鏈和資料檔案中的不同位置。第二,它更新聚集索引鍵的行編號。這個行編號被儲存,而且被再次更新在非聚集索引裡。就I/O而言,這可能非常昂貴,尤其是在批處理更新的情況下。另外,它還會增加聚集索引的碎片,在行編號增加下,還增加非聚集索引的碎片。因此,最好有一個靜態聚集索引,讓鍵的值不會改變。

  所有非聚集索引使用聚集索引鍵作為行編號。一個太大的聚集索引鍵增加非聚集索引行的儲存空間。結果,在索引和距離掃描時SQL server不得不處理更多的資料頁,這將導致效率低下。

  在非唯一非聚集索引情況下,行編號同樣儲存在非葉索引層上,反過來,這將減少每頁的索引記錄,並且導致索引中額外的中間級別。在每次SQLserver遍歷非聚集索引的二叉樹時,儘管非葉索引層快取在記憶體中,但是都會引入額外的邏輯讀取。

  最後,在索引維護時,越來越多的非聚集索引在緩衝池中使用更多空間和引入更多的開銷。顯然,不可能提供一個通用閾值來定義可以應用於任何表的鍵的最大可接受大小。但是,一般來說,最好使用一個窄的聚集索引鍵,索引鍵越小越好。

  將索引鍵定義為唯一的大有益處。這個重要原因並非顯而易見的。考慮這個場景,在表沒有唯一的索引時,而你希望執行使用執行計劃中非聚集索引查詢的查詢。

  在這個情況下,如果非聚集索引中的行編號不是惟一的,SQL Server將不知道在鍵查詢操作期間選擇哪個聚集索引行。SQL Server通過向非惟一聚集索引中新增另一個名為唯一識別符號的可空整數列來解決此類問題。對於鍵值的第一次出現,SQL Server用NULL填充唯一識別符號,併為插入到表中的每個後續重複值自動遞增。

 


□注意:每個聚集索引鍵的值的可能的重複次數受整型域值的限制。你不能有超過2,147,483,648行使用相同的聚集索引鍵。這是一個理論上的限制,建立選擇性如此差的索引顯然不是一個好主意。


  

讓我們看看在非惟一聚集索引中引入唯一識別符號的開銷。清單7-1所示的程式碼建立了三個相同結構的不同表,每個表都填充了65,536行。UniqueCI表是唯一被定義了唯一聚集索引。nonuniquecinodups表沒有任何重複的鍵值。最後,NonUniqueCDups表在索引中有大量的副本。

清單7 - 1非惟一聚集索引:表建立

 

   現在,讓我們看看每個表的聚集索引的物理統計資訊。程式碼如清單7-2所示,結果如圖7-1所示。

清單7 - 1非唯一聚集索引:檢查聚集索引的行大小

  儘管在表NonUniqueCINoDups沒有重複的鍵,但是行仍然增加了2個額外的位元組。SQL Server在資料的變長部分儲存一個唯一識別符號,這2個位元組由變長資料偏移陣列中的另一個條目新增。

  在這種情況下,當聚集索引具有重複值時,唯一識別符號將再新增4個位元組,即造成總共6個位元組的開銷。值得一提的是,在某些特殊情況下,唯一識別符號使用的額外儲存空間可以減少資料頁上可以容納的行數。我們的示例演示了這種情況。如你所見,UniqueCI表與其他兩個表相比,UniqueCI表使用的資料頁少了大約15%。206/5000  

  現在,讓我們看唯一識別符號如何影響非聚集索引。清單7-3所示的程式碼在所有三個表中建立了非叢集索引。圖7-2顯示了這些索引的物理統計資訊。

   清單7-3非唯一聚集索引:檢查非聚集索引的行大小

圖7 - 2非唯一聚集索引:非聚集索引的行大小

  在NonUniqueCINoDups表沒有非聚集索引沒有開銷。正如你所記得的,SQLserver不會儲存偏移量資訊到為了儲存空資料的尾隨列的變長陣列中。儘管如此,在NonUniqueCIDups表中唯一識別符號引入了8子節的開銷。這8個位元組由一個4位元組的唯一識別符號值、一個2位元組的變長資料偏移陣列條目和一個儲存行中變長列數的2位元組條目組成。我們可以用以下方式總結唯一識別符號的儲存開銷。對於唯一識別符號為空的行,如果索引中至少有一個儲存非空值的變長列,則會有兩個位元組的開銷。這個開銷來自變長偏移陣列條目為了唯一識別符號所在行,否則將不會有開銷。在填充唯一識別符號的情況下,如果存在儲存非空值的變長列,則開銷為6位元組。否則,開銷是8位元組。

 


 

□提示:如果期望在聚集索引值中有大量重複,可以將整數標識列作為最右邊的列新增到索引中,從而使其惟一。與唯一識別符號引入的最多8位元組的不可預測儲存開銷相比,這將為每行增加一個4位元組的可預測儲存開銷。當您通過引用該行的所有聚集索引列時,還可以提高單個查詢操作的效能。


 

  

  以最小化插入新行導致的索引碎片的方式設計聚集索引是有益的。實現這一點的方法之一是使聚集索引值不斷增加。標識列上的索引就是這樣一個例子。另一個例子是時間列,其中填充了插入時的當前系統時間。

  然而,索引不斷增長存在兩個潛在問題。第一個與統計有關。

  正如第三章所學的,當直方圖中沒有引數值時,SQL Server中的遺留基數估計值會低估基數。您應該將這種因素考慮到系統的統計維護策略中,除非您使用新的SQL Server 2014-2016基數估計值,該估計值假定直方圖之外的資料具有類似於表中其他資料的分佈。下一個問題更復雜。隨著索引的增加,資料總是插入索引的末尾。一方面,它可以防止頁面分裂,減少碎片。另一方面,它可能導致熱點,即當多個會話試圖修改相同的資料頁和分配新的頁或區段時發生的序列化延遲。SQL Server不允許多個會話更新相同的資料結構,而是序列化這些操作。熱點通常不是問題,除非系統以非常高的速率收集資料,並且索引每秒處理數百次插入。我們將在第27章“系統故障排除”中討論如何檢測此類問題。

  最後,如果系統有一組頻繁執行的重要查詢,那麼考慮聚集索引可能是有益的,它可以優化這些查詢。這消除了昂貴的鍵查詢操作,並提高了系統的效能。

  儘管可以通過使用覆蓋非聚集索引來優化此類查詢,但它並不總是理想的解決方案。在某些情況下,需要建立非常大的非聚集索引,這將佔用磁碟和緩衝池中的大量儲存空間。另一個重要因素是修改列的頻率。將經常修改的列新增到非聚集索引需要SQL Server在多個位置更改資料,這會對系統的更新效能產生負面影響,並增加阻塞。

  儘管如此,設計滿足所有這些指導原則的聚集索引並不總是可能的。此外,您不應該將這些指導方針視為絕對的要求。您應該分析系統、業務需求、工作負載和查詢,並選擇對您有利的叢集索引,即使它們違反了其中的一些指導原則。

識別符號、序列和惟一識別符號

  人們通常選擇識別符號、序列和惟一識別符號作為聚集索引鍵。與往常一樣,這種方法有其優缺點。在這些列上定義的聚集索引是惟一的、靜態的和窄的。此外,恆等式和序列不斷增加,這減少了索引碎片。其中一個最理想的用例是目錄實體表。例如,可以考慮儲存客戶、文章或裝置列表的表。這些表儲存數千行,甚至可能幾百萬行,儘管資料是相對靜態的,因此,熱點不是問題。此外,這些表通常由外來鍵引用並用於連線。整數列或大數列上的索引非常緊湊和高效,這將提高查詢的效能。

 


 

注意,我們將在第8章“約束”中更詳細地討論外來鍵約束。


 

 

  識別符號或序列列上的聚集索引在事務表的情況下效率較低,事務表以非常高的速率收集大量資料,這是由於它們引入的潛在熱點。另一方面,對於聚集索引和非聚集索引,唯一的識別符號很少是一個好的選擇。使用NEWID()函式生成的隨機值極大地增加了索引碎片。此外,惟一識別符號上的索引會降低批處理操作的效能。讓我們看一個示例並建立兩個表:一個表在標識列上具有聚集索引,另一個表在唯一的識別符號列上具有聚集索引。下一步,我們將在兩個表中插入65,536行。您可以在清單7-4中看到執行此操作的程式碼。

我的計算機上的執行時間和讀取次數如表7-1所示。圖7-3顯示了這兩個查詢的執行計劃。

圖7-3將資料插入表:執行計劃

  如您所見,在唯一的識別符號列上的索引的情況下,還有另一個排序操作符。SQL Server在插入之前對隨機生成的唯一的識別符號值進行排序,這會降低查詢的效能。

  讓我們向表中插入另一批行並檢查索引碎片。執行此操作的程式碼如清單7-5所示。圖7-4顯示了查詢的結果

清單7 - 5惟一識別符號:插入行並檢查碎片

圖7 - 4索引碎片化

  正如您所看到的,唯一識別符號列上的索引非常分散,與識別符號列上的索引相比,它使用的資料頁多了大約40%。在唯一識別符號列上的索引中進行批量插入,會在資料檔案的不同位置插入資料,對於大型表,這會導致大量隨機物理I/O。這樣會大大降低操作的效果。

 


 

個人經驗


 

   

  不久前,我參與了一個系統的優化,該系統有一個250GB的表,其中有一個聚集索引和三個非聚集索引。其中一個非聚集索引是唯一識別符號列上的索引。通過刪除這個索引,我們能夠將50,000行的批插入從45秒提高到7秒。

  當您希望在唯一識別符號列上建立索引時,有兩個常見的用例。第一個用於支援跨多個數據庫的值的惟一性。考慮一個分散式系統,其中行可以插入到每個資料庫中。開發人員經常使用唯一識別符號來確保每個鍵值在系統範圍內是唯一的。

  這種實施方式中的關鍵要素是如何生成鍵值正如您已經看到的,NEWID()函式或客戶機程式碼中生成的隨機值會對系統性能產生負面影響。但是,您可以使用NEWSEQUENTIALID()函式,該函式生成惟一且通常不斷增長的值(SQL Server會不時重置它們的基值)。   

  NEWSEQUENTIALID()函式生成的唯一識別符號上的索引與識別符號和序列列上的索引類似;但是,您應該記住,與4位元組的int或8位元組的長整型資料型別相比,唯一識別符號資料型別使用16位元組的儲存空間。作為一種替代解決方案,您可以考慮建立一個包含兩列(InstallationId、Unique_Id_Within_Installation)的複合索引。這兩列的組合保證了跨多個安裝和資料庫的唯一性,並且比惟一識別符號使用更少的儲存空間。您可以使用整數標識或序列來生成Unique_Id_Within_Installation值,這將減少索引的碎片。

  在需要跨資料庫中所有實體生成惟一鍵值的情況下,可以考慮跨所有實體使用單個sequence物件。這種方法滿足了需求,但是使用了比惟一識別符號更小的資料型別。

  另一個常見的用例是安全性,其中惟一識別符號值用作安全令牌或隨機物件ID。不幸的是,在這個場景中不能使用NEWSEQUENTIALID()函式,因為可以猜測該函式返回的下一個值。在這個場景中,一個可能的改進是使用CHECKSUM()函式建立一個計算列,然後索引它,而不需要在uniqueidentifier列上建立索引。程式碼顯示在清單7-6中。

清單7-6使用CHECKSUM():表結構

 


 

□注意:您可以索引計算列而不需要持久化它。


 

 

  儘管IDX_Articles_ExternalIdCheckSum索引將非常分散,但與惟一識別符號列上的索引(4位元組鍵與16位元組鍵)相比,它將更加緊湊。它還提高了批處理操作的效能,因為排序速度更快,而且需要更少的記憶體。您必須記住的一件事是,CHECKSUM()函式的結果不一定是惟一的。應該將這兩個謂詞包含到查詢中,如清單7-7所示。

清單7-7使用CHECKSUM():選擇資料

 


 

□提示:如果需要索引大於900/ 1700位元組的字串列(這是非聚集索引鍵的最大大小),可以使用相同的技術。即使這樣的索引不支援範圍掃描操作,它也可以用於點查詢。


 

 

 

本翻譯節選《Pro SQL Server Internals, 2nd edition》中的一小節,原文可自行查詢瀏覽。