SQL 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 Bookmarkspa
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列時,索引以下所示:
:- 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的五行在索引中是連續的,每隔一個ProductID / ModifiedDate組合的行也是如此。 你可能會問「爲何甚至包括列?爲何不簡單地將OrderQty,UnitPrice和LineTotal添加到索引鍵?「索引中有這些列但索引鍵中沒有這些列有幾個優勢,例如: •不屬於索引鍵的列不會影響索引內條目的位置。這反過來又減小了讓他們在索引中的開銷。例如,若是行中的ProductID或ModifiedDate值被修改,那麼該行的條目必須在索引內從新定位。可是,若是行中的UnitPricevalue被修改,索引條目仍然須要更新,但不須要移動。 •在索引中查找條目所需的努力較少。 •索引的大小會略小。 •索引的數據分佈統計將更容易維護。 當咱們查看索引的內部結構以及由SQL Server維護的用於優化查詢性能的一些附加信息時,大多數這些優點在之後的級別中將更有意義。 肯定索引列是不是索引鍵的一部分,或只是包含的列,不是您將要作的最重要的索引決定。也就是說,頻繁出如今SELECT列表中但不在查詢的WHERE子句中的列最好放在索引的包含列部分。 成爲覆蓋指標在級別4中,咱們表示與AdventureWorks數據庫的設計者達成協議,決定將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 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
這八行輸出從表中的三十九個「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
--RUN 1:在這裏執行清單5.2(沒有非彙集索引)
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate
ON Sales.SalesOrderDetail (ProductID, ModifiedDate) ;
--RUN 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) ;
--RUN 3:在這裏從新執行清單5.2(包含非聚簇索引)
清單5.3:測試「按產品的活動總計」查詢 表5.1顯示了對每一個索引方案執行查詢所需的相對工做量。
Run 1: No Nonclustered Index |
Table 'SalesOrderDetail'. Scan count 1, logical reads 1238. Non read activity: 8%. |
Run 2: Index – No Included Columns |
Table 'SalesOrderDetail'. Scan count 1, logical reads 131. Non read activity: 0%. |
Run 3: With Included Columns |
Table 'SalesOrderDetail'. Scan count 1, logical reads 3. Non read activity: 1%. |
表5.1:使用不一樣的非彙集索引可運行第一次查詢三次的結果 正如你能夠從這些結果看到的: •運行1須要完整掃描SalesOrderDetail表;每一行都必須閱讀和檢查,以肯定是否應該參與結果。 •運行2使用非聚簇索引爲39個請求的行快速查找書籤,但必須從表中單獨檢索每一個行。 •運行3在非彙集索引中找到所需的全部內容,並以最有利的順序 - 產品ID中的ModifiedDate。它迅速跳到第一個要求的條目,閱讀了39個連續的條目,對每一個條目進行了總計算,讀取完成。 測試第二個查詢:基於日期的活動總數 咱們的第二個查詢與第一個查詢是相同的,除了WHERE子句的更改。此次倉庫正在根據日期而不是產品請求信息。咱們必須過濾最右邊的搜索鍵列ModifiedDate;而不是最左邊的一列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顯示了對每一個索引方案執行查詢所需的相對工做量。
Run 1: No Nonclustered Index |
Table 'SalesOrderDetail'. Scan count 1, logical reads 1238. Non read activity: 10%. |
Run 2: With Index – No Included Columns |
Table 'SalesOrderDetail'. Scan count 1, logical reads 1238. Non read activity: 10%. |
Run 3: With Included Columns |
Table 'SalesOrderDetail'. Scan count 1, logical reads 761. Non read activity: 8%. |
表2:使用可用的不一樣非聚簇索引三次運行第二個查詢的結果 第一次和第二次測試都是相同的計劃。對SaleOrderDetail表的完整掃描。因爲第4級中詳細說明的緣由,WHERE子句沒有足夠的選擇性從非覆蓋索引中受益。並且,包含任何一個組的行都散佈在整個表格中。正在讀表時,每一行都必須與其組相匹配。以及消耗處理器時間和內存的操做。 第三個測試發現了它在非彙集索引中須要的一切;但與前面的查詢不一樣,它沒有找到索引內連續的行。構成每一個單獨組的行在索引內是連續的;可是這些羣體自己分散在指數的長度上。所以,SQL Server掃描索引。 掃描索引而不是表格有兩個好處: •索引小於表,須要更少的讀取。 •行已經分組,須要較少的非閱讀活動。 結論 包含的列使非彙集索引可以覆蓋各類查詢的索引,從而提升這些查詢的性能;有時至關戲劇性。包含的列增長了索引的大小,但在開銷方面增長了不多的內容。任什麼時候候你建立一個非彙集索引,特別是在一個外鍵列上,問本身 - 「我應該在這個索引中包含哪些額外的列?」