1. 程式人生 > >包含列的索引:通往SQL Server索引級別5的階梯

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

工作 引入 reat 第一次 查看 相關數 搜索 映射 執行計劃

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

大衛?杜蘭特2011/07/13

該系列

本文是樓梯系列的一部分:SQL Server索引的階梯

索引是數據庫設計的基礎,並告訴開發人員使用數據庫非常了解設計器的意圖。不幸的是,當性能問題出現時,索引常常被添加到事後。這裏最後是一個簡單的系列文章,它應該能讓任何數據庫專業人員快速“跟上”他們的步伐

前面的級別引入了集群和非聚集索引,突出了每個方面的以下方面::

1.表中的每一行都有一個條目(我們註意到這個規則的例外情況將在以後的級別中被覆蓋)。這些條目總是在索引鍵序列中。

2.在聚集索引中,索引項是表的實際行。

3.在非聚集索引中,條目與數據行分開;並由索引鍵列和書簽值組成,將索引鍵列映射到表的實際行。

前半句是正確的,但不完整。在這個級別中,我們檢查了將附加的列包含到非聚集索引的選項,稱為包含列。在第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書簽

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 =>

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列,索引看起來是這樣的:

:- Search Key Columns -: :--- Included Columns ---: : Bookmark :

ProductID ModifiedDate 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 ?“在索引中有這些列有幾個優點,但索引鍵沒有,比如:

1.不屬於索引鍵的列不會影響索引內條目的位置。這反過來降低了在索引中使用它們的開銷。例如,如果行中的ProductID或ModifiedDate值被修改,那麽該行的條目必須在索引中重新定位。但是,如果在行中的unit定價evalue被修改,那麽索引項仍然需要更新,但它不需要移動。

2.在索引中定位一個條目所需的工作量更少。

3.指數的大小將會稍微小一些。

4.索引的數據分布統計數據將更容易維護。

當我們查看索引的內部結構以及SQL Server維護的一些額外信息以優化查詢性能時,這些優勢在以後的級別中會更有意義。

決定一個索引列是否是索引鍵的一部分,或者僅僅是一個包含的列,並不是您所要做的最重要的索引決定。也就是說,在SELECT列表中經常出現的列,而不是查詢的WHERE子句中最優的列在索引的列中。

成為一種覆蓋指數

在第4級,我們與AdventureWorksdatabase的設計人員達成協議,他們決定讓SalesOrderID / SalesOrderDetailID為SalesOrderDetail表的集群索引。針對此表的大多數查詢將請求按銷售訂單號排序或分組的數據。但是,一些查詢,可能來自倉庫人員,將需要在產品序列中的信息。這些查詢將從清單5.1中顯示的索引中獲益。

為了說明在該索引中包含包含列的潛在好處,我們將查看針對SalesOrderDetailtable的兩個查詢,每個查詢將執行三次,如下:

1.運行1:沒有非聚集索引

2.運行2:使用包含不包含列的非聚集索引(只有兩個鍵列)

3.運行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 ModifiedDate No of Rows Avg Price Total Value

----------- ------------ ----------- -----------------------------
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.運行1需要對SalesOrderDetail表進行完整的掃描;每一行都必須閱讀和檢查,以確定是否應該參與結果。
2.Run 2使用非聚集索引快速查找39個請求行的書簽,但它必須從表中逐個檢索這些行。
3.運行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:“按日期執行的活動總數”查詢
產生的行集,部分是:
ProductID   ModifiedDate    No of Items Avg Price             Total Value
----------- ------------ ----------- --------------------- ----------------
:
:
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掃描索引。
1.掃描索引而不是表格有兩個優點:
2.該指數小於表,要求更少的讀數。
3.這些行已經分組,需要更少的非讀活動。
總結
包含的列使非聚集索引能夠成為各種查詢的索引,從而提高這些查詢的性能;有時會很顯著。包含的列增加了索引的大小,但在開銷方面卻沒有增加。任何時候創建非聚集索引,尤其是在外鍵列上,都要問自己:“在這個索引中應該包含哪些額外的列?”
 
鏈接:http://www.sqlservercentral.com/articles/Stairway+Series/72276/

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