包含列的索引:通往SQL Server索引級別5的階梯
包含列的索引:通往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的階梯