原文連接:http://www.sqlservercentral.com/articles/Stairway+Series/72276/sql
包含列的索引:通往SQL Server索引級別5的階梯數據庫
大衛•杜蘭特2011/07/13數據庫設計
該系列sqlserver
本文是樓梯系列的一部分:SQL Server索引的階梯性能
索引是數據庫設計的基礎,並告訴開發人員使用數據庫很是瞭解設計器的意圖。不幸的是,當性能問題出現時,索引經常被添加到過後。這裏最後是一個簡單的系列文章,它應該能讓任何數據庫專業人員快速「跟上」他們的步伐測試
前面的級別引入了集羣和非彙集索引,突出了每一個方面的如下方面:優化
表中的每一行都有一個條目(咱們注意到這個規則的例外狀況將在之後的級別中被覆蓋)。這些條目老是在索引鍵序列中。設計
在彙集索引中,索引項是表的實際行。server
在非彙集索引中,條目與數據行分開;並由索引鍵列和書籤值組成,將索引鍵列映射到表的實際行。排序
前半句是正確的,但不完整。在這個級別中,咱們檢查了將附加的列包含到非彙集索引的選項,稱爲包含列。在第6級檢查書籤操做時,咱們會看到SQL Server可能會單方面向索引添加一些列。
包括列
非彙集索引中的列,但不是索引鍵的一部分,被稱爲包含列。這些列不是鍵的一部分,所以不影響索引中的條目序列。並且,正如咱們將看到的,它們比鍵列的開銷更少。
在建立非彙集索引時,咱們將分別從鍵列指定包含的列;如清單5.1所示。
CREATE NONCLUSTERED INDEX FK_ProductID_ ModifiedDate
ON Sales.SalesOrderDetail (ProductID, ModifiedDate)
INCLUDE (OrderQty, UnitPrice, LineTotal)
清單5.1:建立包含列的非彙集索引
在本例中,ProductID和ModifiedDate是索引鍵列,OrderQty、UnitPrice和LineTotal是包含的列。
若是咱們沒有在上面的SQL語句中指定INCLUDE子句,那麼結果的索引應該是這樣的:
ProductID ModifiedDate書籤
Page n:
707 2004/07/25 =>
707 2004/07/26 =>
707 2004/07/26 =>
707 2004/07/26 =>
707 2004/07/27 =>
707 2004/07/27 =>
707 2004/07/27 =>
707 2004/07/28 =>
707 2004/07/28 =>
707 2004/07/28 =>
707 2004/07/28 =>
707 2004/07/28 =>
707 2004/07/28 =>
Page n+1:
707 2004/07/29 =>
707 2004/07/31 =>
707 2004/07/31 =>
707 2004/07/31 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
708 2001/07/01 =>
然而,已經告訴SQL Server包括OrderQty、UnitPrice和LineTotal列,索引看起來是這樣的:
--- --- --- --- --- --- --- --- --- --- --- --
產品修改日期
Page n-1:
707 2004/07/29 1 34.99 34.99 =>
707 2004/07/31 1 34.99 34.99 =>
707 2004/07/31 3 34.99 104.97 =>
707 2004/07/31 1 34.99 34.99 =>
708 2001/07/01 5 20.19 100.95 =>
Page n:
708 2001/07/01 1 20.19 20.19 =>
708 2001/07/01 1 20.19 20.19 =>
708 2001/07/01 2 20.19 40.38 =>
708 2001/07/01 1 20.19 20.19 =>
708 2001/07/01 2 20.19 40.38 =>
708 2001/12/01 7 20.19 141.33 =>
708 2001/12/01 1 20.19 20.19 =>
708 2002/01/01 1 20.19 20.19 =>
708 2002/01/01 1 20.19 20.19 =>
708 2002/01/01 1 20.19 20.19 =>
Page n+1:
708 2002/01/01 2 20.19 40.38 =>
708 2002/01/01 5 20.19 100.95 =>
708 2002/02/01 1 20.19 20.19 =>
708 2002/02/01 1 20.19 20.19 =>
708 2002/02/01 2 20.19 40.38 =>
檢查這個索引的內容,很明顯,這些行是由索引鍵列排序的。例如,在2002年1月1日修改後的產品708(以粗體顯示)的5行,在索引中是連續的,就像其餘全部ProductID / ModifiedDate組合中的行同樣。
你可能會問「爲何要包含列呢?」爲何不直接向索引鍵添加OrderQty、UnitPrice和LineTotal ?「在索引中有這些列有幾個優勢,但索引鍵沒有,好比:
不屬於索引鍵的列不會影響索引內條目的位置。這反過來下降了在索引中使用它們的開銷。例如,若是行中的ProductID或ModifiedDate值被修改,那麼該行的條目必須在索引中從新定位。可是,若是在行中的unit訂價evalue被修改,那麼索引項仍然須要更新,但它不須要移動。
在索引中定位一個條目所需的工做量更少。
指數的大小將會稍微小一些。
索引的數據分佈統計數據將更容易維護。
當咱們查看索引的內部結構以及SQL Server維護的一些額外信息以優化查詢性能時,這些優點在之後的級別中會更有意義。
決定一個索引列是不是索引鍵的一部分,或者僅僅是一個包含的列,並非您所要作的最重要的索引決定。也就是說,在SELECT列表中常常出現的列,而不是查詢的WHERE子句中最優的列在索引的列中。
成爲一種覆蓋指數
在第4級,咱們與AdventureWorksdatabase的設計人員達成協議,他們決定讓SalesOrderID / SalesOrderDetailID爲SalesOrderDetail表的集羣索引。針對此表的大多數查詢將請求按銷售訂單號排序或分組的數據。可是,一些查詢,可能來自倉庫人員,將須要在產品序列中的信息。這些查詢將從清單5.1中顯示的索引中獲益。
爲了說明在該索引中包含包含列的潛在好處,咱們將查看針對SalesOrderDetailtable的兩個查詢,每一個查詢將執行三次,以下:
運行1:沒有非彙集索引
運行2:使用包含不包含列的非彙集索引(只有兩個鍵列)
運行3:使用清單5.1中定義的非彙集索引
正如咱們在之前的級別中所作的那樣,咱們再次使用讀做爲主要度量,可是咱們也使用SQL Server Management Studio的「顯示實際執行計劃」選項來查看每一個執行的計劃。這將給咱們一個額外的度量:在非讀取活動上花費的工做量的百分比,例如在讀入內存以後匹配相關數據。這使咱們更好地理解了查詢的總成本。
測試第一個查詢:活動總數按產品
咱們的第一個查詢,如清單5.2所示,是一個爲特定產品提供活動總數的查詢。
SELECT ProductID ,
ModifiedDate ,
SUM(OrderQty) AS 'No of Items' ,
AVG(UnitPrice) 'Avg Price' ,
SUM(LineTotal) 'Total Value'
FROM Sales.SalesOrderDetail
WHERE ProductID = 888
GROUP BY ProductID ,
ModifiedDate ;
清單5.2:「產品的活動總數」查詢
由於索引能夠影響查詢的性能,但不能影響結果;針對這三種不一樣的索引方案執行此查詢老是會產生如下行集:
ProductID修改日期不爲全部行Avg價格總值
----------- ------------ ----------- -----------------------------
888 2003-07-01 16 602.346 9637.536000
888 2003-08-01 13 602.346 7830.498000
888 2003-09-01 19 602.346 11444.574000
888 2003-10-01 2 602.346 1204.692000
888 2003-11-01 17 602.346 10239.882000
888 2003-12-01 4 602.346 2409.384000
888 2004-05-01 10 602.346 6023.460000
888 2004-06-01 2 602.346 1204.692000
8行輸出從表中的39個「ProductID = 888」行聚合到每一個有一個或多個「ProductID = 888」銷售的日期的輸出行。進行測試的基本方案如清單5.3所示。在運行任何查詢以前,確保運行SET STATISTICS IO ON。
IF EXISTS ( SELECT 1
FROM sys.indexes
WHERE name = 'FK_ProductID_ModifiedDate'
AND OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') )
DROP INDEX Sales.SalesOrderDetail.FK_ProductID_ModifiedDate ;
GO
——運行1:在這裏執行清單5.2(沒有非彙集索引)
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
ON Sales.SalesOrderDetail (ProductID, ModifiedDate) ;
——運行2:在這裏從新執行清單5.2(非集羣索引,不包含任何內容)
IF EXISTS ( SELECT 1
FROM sys.indexes
WHERE name = 'FK_ProductID_ModifiedDate'
AND OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') )
DROP INDEX Sales.SalesOrderDetail.FK_ProductID_ModifiedDate ;
GO
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
ON Sales.SalesOrderDetail (ProductID, ModifiedDate)
INCLUDE (OrderQty, UnitPrice, LineTotal) ;
——運行3:在這裏從新執行清單5.2(包含包含的非彙集索引)
清單5.3:測試「產品的活動總數」查詢
對每一個索引方案執行查詢所需的相對工做如表5.1所示。
1:運行
沒有非彙集索引
表「SalesOrderDetail」。掃描計數1,邏輯讀1238。
非閱讀活動:8%。
運行2:
索引-不包括列
表「SalesOrderDetail」。掃描計數1,邏輯讀131。
非閱讀活動:0%。
運行3:
包括列
表「SalesOrderDetail」。掃描計數1,邏輯讀3。
非閱讀活動:1%。
表5.1:使用不一樣的非彙集索引運行第一個查詢的結果三次
從這些結果能夠看出:
運行1須要對SalesOrderDetail表進行完整的掃描;每一行都必須閱讀和檢查,以肯定是否應該參與結果。
Run 2使用非彙集索引快速查找39個請求行的書籤,但它必須從表中逐個檢索這些行。
運行3在非彙集索引中找到所需的全部內容,並在ProductID內最有利的序列中進行修改。它迅速跳到第一個請求的條目,讀了39個連續的條目,在讀取的每一個條目上作彙總計算,而後完成了。
測試第二個查詢:基於日期的活動總數
咱們的第二個查詢與第一個查詢徹底相同,只是在WHERE子句中發生了更改。這一次,倉庫是根據日期請求信息,而不是基於產品。咱們必須在最右的搜索鍵欄上進行過濾,修改日期;而不是最左邊的列,ProductID。新的查詢如清單5.4所示。
SELECT ModifiedDate ,
ProductID ,
SUM(OrderQty) 'No of Items' ,
AVG(UnitPrice) 'Avg Price' ,
SUM(LineTotal) 'Total Value'
FROM Sales.SalesOrderDetail
WHERE ModifiedDate = '2003-10-01'
GROUP BY ModifiedDate ,
ProductID ;
清單5.4:「按日期執行的活動總數」查詢
產生的行集,部分是:
產品的修改日期不包括價格總額
----------- ------------ ----------- --------------------- ----------------
:
:
782 2003-10-01 62 1430.9937 86291.624000
783 2003-10-01 72 1427.9937 100061.564000
784 2003-10-01 52 1376.994 71603.688000
792 2003-10-01 12 1466.01 17592.120000
793 2003-10-01 46 1466.01 67436.460000
794 2003-10-01 37 1466.01 54242.370000
795 2003-10-01 22 1466.01 32252.220000
:
:
(164 row(s) affected)
WHERE子句將表過濾到1492行;在分組時,生成了164行輸出。
要運行測試,請遵循清單5.3中描述的相同方案,可是使用清單5.4中的新查詢。結果是針對每一個索引方案執行查詢所需的相對工做,如表5.2所示。
1:運行
沒有非彙集索引
表「SalesOrderDetail」。掃描計數1,邏輯讀1238。
非閱讀活動:10%。
運行2:
索引-不包括列
表「SalesOrderDetail」。掃描計數1,邏輯讀1238。
非閱讀活動:10%。
運行3:
包括列
表「SalesOrderDetail」。掃描計數1,邏輯讀761。
非閱讀活動:8%。
表2:使用不一樣的非彙集索引運行第二個查詢的結果
第一次和第二次測試都產生了相同的計劃;一個完整的掃描詳細信息表。因爲第4級中詳細討論的緣由,WHERE子句沒有足夠的選擇性從非覆蓋索引中獲益。並且,包含任何一個組的行分佈在整個表中。在讀取表時,每一行必須與組相匹配;以及消耗處理器時間和內存的操做。
第三個測試在非彙集索引中找到了它所須要的一切;可是,與前面的查詢不一樣,它沒有發現索引中相鄰的行。在索引中,包含每一個組的行是連續的;但這些組織自己分散在指數的長度上。所以,SQL Server掃描索引。
掃描索引而不是表格有兩個優勢:
該指數小於表,要求更少的讀數。
這些行已經分組,須要更少的非讀活動。
結論
包含的列使非彙集索引可以成爲各類查詢的索引,從而提升這些查詢的性能;有時會很顯著。包含的列增長了索引的大小,但在開銷方面卻沒有增長。任什麼時候候建立非彙集索引,尤爲是在外鍵列上,都要問本身:「在這個索引中應該包含哪些額外的列?」
本文是通往SQL Server索引樓梯的樓梯的一部分