包含列的索引:通往SQL Server索引級別5的階梯

原文連接: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索引樓梯的樓梯的一部分

相關文章
相關標籤/搜索