1. 程式人生 > >第七章——設計和調整索引

第七章——設計和調整索引

  要定義一個在任何地方都有效的索引策略是不可能的。因為每個系統都是獨一無二的,都需要基於工作量的索引方法,業務需求和其他的一些因素。然而,有一些設計方面的考慮和準則,可以適用於每一個系統。  

當我們要優化現有系統時,也是這樣。而優化是一個迭代過程,每個案例中都是獨一無二的,有一系列技術可以用來檢測資料庫系統每個案例中的低效率的例子。

在本章中,我們將介紹一些重要的因素,在設計新索引和優化現有系統時需要牢記這些因素。

分類索引設計考慮因素

每次更改聚集索引鍵的值,都會發生兩件事。第一件事就是sql伺服器在聚集索引頁鏈和資料檔案中把行移動到不同位置。第二,它更新聚集索引鍵的行號。所有非聚集索引都要儲存、更新行標識。但就輸入

/輸出埠而言,在批量更新的情況下會特別很貴。而且,在行id大小增加的情況下,它會增加群集索引和非群集索引的碎片化。因此,最好是有一個靜態的聚集索引,因為它關鍵值不會改變。

所有非聚集索引都使用聚集索引鍵作為行id。太寬泛的聚集索引鍵會增加非聚集索引行的大小,而且需要更多空間來儲存它們。結果就是sql伺服器在索引或範圍掃描操作中需要處理更多的資料頁,會索引降低效率。在非唯一非聚集索引的情況下,行id也儲存在非葉索引級別,反過來,減少每頁索引記錄的數量,會導致索引中額外的中間級別。即使非葉索引級別通常快取在記憶體中,這也導致了每次sql伺服器遍歷非聚集索引b樹會有額外的邏輯讀取。

最後一點,較大的非聚集索引消耗緩衝池中更多的空間,還會在索引維護期間使用更多的開銷。顯然,不可能用通用閾值,是指用於任何表的的鍵可接受的最大值。但是,作為通用規則,最好有一個索引鍵儘可能小的聚集索引鍵。

將群集索引定義為唯一值也是有益的。考慮一下這樣的情況,表沒有唯一的聚集索引,而您希望在執行計劃中執行一個使用非聚集索引搜尋的查詢。在這種情況下,如果非聚集索引中的行id不是唯一的,sql伺服器就不知道選擇哪個聚集索引行來進行金鑰查詢操作。

sql伺服器通過為非唯一聚集索引新增一個叫做統一片語的可以為空的整數列來解決這些問題。sql伺服器為首次出現的鍵值填充了空單元,並且為插入到表中的每個後續副本自動遞增。

筆記:每組集索引鍵值可能重複的次數受到整數域值的限制。您不能有多於2,147,483,648行的相同聚集索引鍵。這是一個理論上的限制,顯然創造如此低的選擇性的索引是一個錯誤的想法。

讓我們來看看在非唯一的聚集索引中由集合符引入的開銷,表7-1中顯示的程式碼建立了三個相同結構的不同表,每個表填充了65,536行。SQL查詢語句:Table dbo.UniqueCI 是唯一一張有定義唯一聚集索引的表。 Table dbo.NonUniqueCINoDups沒有重複的鍵值。最後,table dbo.NonUniqueCDups在索引中有大量的重複。

現在,讓我們看看每個表的聚集索引的物理統計。這方面的程式碼如

7-2,結果在圖7-1中。

即使dbo.NonUniqueCINoDups 表中沒有重複的鍵值,仍然有兩個新增到行中的額外位元組。sql伺服器在資料的可變長度部分儲存一個全域性唯一識別符號,而這兩個位元組是由可變長度資料偏移陣列中的另一個記錄新增的。

在這種情況下,如果叢集索引具有重複的值,則全域性唯一識別符號將新增另外四個位元組,從而使總開銷達到6個位元組。

值得一提的是,在一些極端情況下,全域性唯一識別符號使用的額外儲存空間可以減少可以適合資料頁的行數。我們的例子說明了這種情況。正如你所看到的,dbo.laxieci使用的資料頁比其他兩個表少15%。現在,讓我們來看看全域性唯一識別符號如何影響非聚集索引。表7-3的程式碼在三個表中都建立了非聚集索引。圖7-2是這些索引的物理統計。

dbo.非獨立的函式表中的非聚集索引中沒有開銷。你應該記得,sql伺服器不為冗餘列儲存空資料在可變長偏移陣列中儲存偏移量資訊。

儘管如此,全域性唯一識別符號還是在dbo.非獨立配置表中引入了8個位元組的開銷。這八個位元組由四位元組全域性變數符的值和兩位元組可變長度資料偏移陣列記錄,以及兩位元組儲存行中可變長列數的記錄組成。

我們可以用下面的方法來總結這個唯一識別符號的儲存開銷。對於包含一個空唯一識別符號的行,如果索引至少有一個儲存非空值的可變長列,則有兩個位元組的開銷。開銷來自單位列的可變長度偏移陣列項。除此以外沒有其他開銷了。在唯一識別符號被填充的情況下,如果可變長列儲存非空值,則開銷為6個位元組,否則為8位元組。

如果您希望在群集索引值中有大量重複,則可以新增整數標識列作為索引的最右邊列,從而使其唯一。這將給每行增加一個四位元組的可預測儲存開銷,而唯一識別符號引入了一個不可預測的高達八位元組的儲存開銷。這也可以改善單個查詢操作的效能,當你通過所有的聚集索引列引用該行。

在設計聚集索引時,最好儘量減少插入新行造成的索引碎片。實現這一目標的其中一個方法是使聚集的索引值不斷增加。標識上的索引就是這樣一個例子。另一個例子是記錄插入時填充了當前系統時間的日期時間的列。然而,隨著指標不斷地增加,有兩個潛在的問題。第一個問題與統計有關。你在第三章學過,當直方圖中沒有引數值時,sql伺服器中的傳統基數估計器對基數估計不足。你應該在系統的統計維護策略中考慮這種行為,除非您使用的是2014-2016年新的sql伺服器的基數估計,它能假設直方圖之外的資料具有與表中其他資料相似的分佈。

下一個問題更復雜。在索引不斷增加的情況下,資料總是插入到索引的末尾。一方面,它可以防止頁面拆分、減少碎片,另一方面,它會置於危險,就是序列化延遲,當多個會話試圖修改同一資料頁和/或分配新頁面或區域時會發生序列化延遲。sql伺服器不允許多個會話更新相同的資料結構,只能進行序列化操作。

熱點通常不是問題,除非系統以非常高的速度收集資料,並且索引每秒處理數百次資料插入。我們將在第27章“系統故障排除”中討論如何檢測這樣的問題。

最後,如果一個系統有一組需要經常執行的重要查詢語句,最好是考慮群集索引,因為它可以優化這些索引。群集索引能消除高消耗的查詢鍵操作,提高了系統的效能。

雖然可以通過覆蓋非聚集索引優化此類查詢,但它並不是在每個情況下都很理想的解決方案。有時候它需要建立非常寬泛的非聚集索引,這樣會消耗磁碟和緩衝池中的大量儲存空間。

另一個重要因素是修改列的頻率。把經常修改的列新增到非聚集索引中,需要sql伺服器在多個地方更改資料,這樣會對系統的更新效能產生負面影響而且增加阻塞。

儘管如此,不是每次都能設計出能夠滿足所有要求的叢集索引的準則。此外,不要把這些準則當做絕對的要求。你要分析系統,業務需求,工作量和查詢,然後選擇對您有利的叢集索引,即使你的選擇會違反準則。

 

標識、序列和全域性唯一識別符號

我們經常用標識、序列和單元素識別符號作為聚集索引鍵。和其他情況一樣,這種做法有它自己的利弊。

定義在此類列上的聚集索引是唯一的、靜態的、狹窄的。此外,標識和序列不斷增加可以減少指指標碎片。

理想用例之一是目錄實體表。可以把儲存了客戶、文章或裝置的表當成示例。這些表儲存了幾千行,甚至幾百萬行,雖然資料相對是靜態的,但熱點不是問題。另外,這類表格通常用外來鍵連線.非常簡潔有效的整數或大整數列的索引能提高查詢的效能。

筆記:我們將在第8章“約束”中更詳細地討論外來鍵約束。

在事務表下,標識列或序列列上的聚集索引的效率比較低,它們高速收集大量資料,所以引入了潛在的熱點。Uniqueidentifiers,在另一方面,一般不用唯一識別符號作為聚集或非聚集索引的物件。用newid()函式生成的隨機值大大增加了索引碎片。另外,唯一識別符號降低了批量操作的效能。我們看一個示例來建立兩個表:一個在標識列上有聚集索引,另一個在統一標識列上有聚集索引。在下一步,我們將在兩個表中插入65,536行記錄。您可以在表7-4中看到步驟的程式碼。

在我電腦上的執行時間和讀取次數列在表7-1上。圖7-3顯示了這兩個查詢的執行計劃。

如圖所示,在唯一標識列上的索引中還有另一個排序操作符。sql伺服器在插入記錄前對隨機生成的單一標識值進行排序會降低查詢的效能。把另一批行插入表中,並檢查索引碎片。步驟程式碼在清單7-5中。圖7-4顯示查詢的結果。

如圖所示,單一標識列上的索引非常分散,用超過40%的資料頁和標識頁的指標對比。

批量處理插入單一識別符號列的索引,會在檔案的多個位置插入資料,如果有大量表格,就會導致過重、隨機物理輸入輸出流。這樣會大大降低操作的效能。

個人經驗

以前,我參與了一個系統的優化,該系統有一個250gb表,其中有一個叢集索引和三個非叢集索引。其中一個非聚集的索引是單一標識列上的索引。刪除這個索引以後,我們可以把50,000行的批插入從45秒加速到7秒。

 

有兩個常見的情況,用與你在單一標識列上建立索引的時候。第一個是支援多個數據庫中值的唯一性。想象一個可以將行插入到每個資料庫中的分散式系統。開發人員經常使用單一識別符號來確保每個關鍵值都是唯一的系統範圍。

實現的關鍵要素是如何生成關鍵值。如你所見,用newid()函式或客戶端程式碼生成的隨機值會對系統性能產生負面影響。但是,您可以使用新序列函式id: NEWSEQUENTIALID() ,它生成唯一的通用的遞增值(sql伺服器時不時地重新設定它的基本值)。使用電子序列EWSEQUENTIALID() 函式生成的單列標識列的索引類似於標識列和序列列的索引;但是,你要記住,單位標識器資料型別使用了16位元組的儲存空間,而INT型別只用4位元組,bigint型別用8位元組。

你可以考慮建立一個包含兩列的複合索引(InstallationId安裝編號, Unique_Id_Within_Installation獨有的安裝編號)作為替代解決方案。這兩列的組合保證了多個裝置和資料庫的唯一性,而且使用的儲存空間比單列識別符號更少。可以用整數標識或序列生成Unique_Id_Within_Installation獨有的安裝編號,減少索引的碎片。

如果需要在資料庫中的所有實體中生成唯一的鍵值,可以考慮在所有實體中使用單個序列物件。這個方法滿足要求,但用的資料型別要比單一識別符號小。另一個常見的例子是安全性,其中一個單位識別符號值用作安全性令牌或隨機物件id。遺憾的是,在這個例子中不能用新序列函式NEWSEQUENTIALID(),因為可以猜出該函式返回的下一個值。一個可行的改進方案是使用檢查函式CHECKSUM()建立一個計算過的列,不用再單位標識列上建立索引,而是在此之後將其索引。程式碼如7-6

技巧:您可以索引計算過的列,就不用把它永久化。

即使 IDX_Articles_ExternalIdCheckSum 的指標會嚴重碎片化,它與單位元組標識欄上的索引(4位元組的鍵比16位元組)相比,它會更加緊湊。因為排序速度更快也提高了批處理操作的效能,可以用更少的記憶體來進行。必須記住的一點,校驗和函式 CHECKSUM()的結果不能保證是唯一的。要把預測都包含到查詢中。如表7-7.

當你需要索引大於900/1,700位元組(非聚集索引鍵的最大大小)的字串列時,可以使用同樣的技術,雖然這樣的索引不支援範圍掃描操作,它也可以用於點查詢。