SQL Server索引進階第二篇:深入非聚集索引

本系列文章的第一篇介紹了SQL Server的索引,尤其重點介紹了非聚集索引,在我們的第一個例子中展示了使用非聚集索引從一個表中取得一行數據所帶來的潛在好處。在本篇文章中,我們繼續研究非聚集索引,本篇文章所研究的內容就要比使用非聚集索引在單表中查詢一行所帶來的性能提升更深一步了。

本系列文章將要列舉的一些例子中介紹的部分理論是關於是非聚集索引的理論,並通過探究索引的內部結構來幫助更好的理解這些理論,在此基礎上,我們分別在存在索引和不存在索引的情況下分別執行相同的查詢並通過統計數據來比較性能。因此我們就可以體會到索引帶來的影響了。

我們繼續使用在第一篇文章中曾使用過的AdventureWorks內的部分數據。尤其是Contact表,我們僅僅使用一個我們在上篇文中中使用過的FullName索引。爲了更好的測試非聚集索引帶來的影響,我將建兩個Contact表,其中一個存在FullName非聚集索引,而另一個不存在。總之就是,兩個相同的表,一個表中存在非聚集索引,另一個不存在非聚集索引。

注意:本篇文章中的T-SQL代碼都可以在文章底部找到下載鏈接

列表1所示代碼創建了Person.Contact表的副本,如果你想恢復到初始測試狀態,你可以隨時運行這段代碼。
  1. IF EXISTS ( SELECT *
  2. FROM sys.tables
  3. WHERE OBJECT_ID = OBJECT_ID('dbo.Contacts_index') ) 
  4. DROP TABLE dbo.Contacts_index ;
  5. GO
  6. IF EXISTS ( SELECT *
  7. FROM sys.tables
  8. WHERE OBJECT_ID = OBJECT_ID('dbo.Contacts_noindex') ) 
  9. DROP TABLE dbo.Contacts_noindex ;
  10. GO
  11. SELECT *
  12. INTO dbo.Contacts_index
  13. FROM Person.Contact ;
  14. SELECT *
  15. INTO dbo.Contacts_noindex
  16. FROM Person.Contact ;
複製代碼
代碼2.1:製作Person.Contact 表的副本

Contacts表內的部分數據如下所示:



非聚集索引條目
如下代碼段在Contacts_index表上創建名爲FullName的非聚集索引.
  1. CREATE INDEX FullName
  2. ON Contacts_index
  3. ( LastName, FirstName ) ;
複製代碼
代碼段2.2 -創建非聚集索引

不要忘了非聚集索引按順序存儲索引鍵。就像書籤可以用於直接訪問表中的數據那樣,你也可以把書籤想象成一種指針,在接下來的文章中我們將更詳細的討論書籤的組成和用法。

FullName索引的部分數據如下,由LastName和FirstName作爲索引鍵外加一個書籤組成。

每一個條目都由索引鍵列和書籤值組成。除此之外,SQL Server中非聚集索引條目還包含了一些可選的頭數據,用於SQL Server內部使用。這兩個非聚集索引條目的組成部分對於理解非聚集索引基礎並不重要,所以安排在後續系列文章中進行講解。

索引條目有序帶來的好處

索引條目按照索引鍵的值有序排列,這樣SQL Server就能快速有序的遍歷索引條目。對於有序的條目的掃描操作既可以從頭到尾,也可以從尾到頭,甚至是從任意條目開始。
因此,如果一個查詢請求想要得到contacts表中lastname以字母s開頭的數據(WHERE LastName LIKE 'S%'),SQL Server會直接找到第一條s開頭的條目(“Sabella, Deanna”)並以此爲出發點進行掃描,直到掃描到第一條以T開頭的條目,這時SQL Server就知道s開頭的條目全部掃描完成,然後使用書籤訪問數據。
上面的請求的列中如果僅僅包含在非聚集索引中,那麼這個查詢會執行的更快
  1. SELECT FirstName ,
  2. LastName
  3. FROM Contact
  4. WHERE LastName LIKE 'S%' ;
複製代碼
SQL Server可以快速的導航到第一條S開頭的條目,然後在無視書籤的情況下遍歷索引條目並直接從索引中取得數據,直到遇到T開頭的索引條目時掃描結束。在關係數據庫的術語中,這個索引“覆蓋”了查詢請求。

任何由順序數據給SQL操作帶來的好處也可以同樣由索引帶來,這些操作包括 ORDER BY, GROUP BY, DISTINCT, UNION (不是UNION ALL), 以及JOIN…ON。
比如,SQL Server使用聚合函數Count根據LastName列來查詢Contact表有多少個聯繫人。就像前面的例子一樣,這是一個覆蓋索引,SQL Server無視了Contact表,僅僅從索引取得所需數據。

值得注意的是鍵列的順序是由左到右的。也就是說前面所建的FullName索引如果按照非聚集索引鍵第一列LastName列作爲查詢條件時將會非常有用,而如果以FirstName作爲查詢條件或許起的作用就不是那麼大了。

測試示例查詢
如果你想執行下面的查詢語句,請確保你首先按照前面的代碼創建了dbo.Contacts_index 和dbo.Contacts_noindex這兩個表,並創建了順序爲LastName, FirstName 的非聚集索引dbo.Contacts_index.

爲了證實我前面的理論,我通過下面代碼開啓統計數據並在有非聚集索引和沒有非聚集索引存在的情況下執行相同的數據。
  1. SET STATISTICS io ON
  2. SET STATISTICS time ON
複製代碼
因爲AdventureWorks數據庫的Contact表中僅有19972行數據,因此很難從時間統計中看出倪端,測試的大部分執行語句CPU時間都是0,因此就不再顯示CPU時間了。僅僅顯示能反映出可能讀取的頁數的IO統計。這個值可以幫助我們對比同樣查詢在存在非聚集索引和不存在非聚集索引的情況下比較同樣查詢語句的性能。如果你想做一些更接近實際的CPU時間測試,在文章的末尾可以找到百萬級別的Contact表的創建代碼。下面的結論僅僅是針對標準的19972行的Contact表。

測試查詢覆蓋
第一條查詢是取出Contact表中LastName列以S開頭的數據,僅僅取出FirstName列和LastName列,因此可以被索引覆蓋,如表2.1所示。

SQL語句 SELECT FirstName, LastName
FROM dbo.Contacts –-分別執行在Contacts_noindex表和Contacts_index表上 
WHERE LastName LIKE 'S%'
沒有非聚集索引 (2130 行受影響)
表 'Contacts_noindex'。掃描計數 1,邏輯讀取 568 次
有非聚集索引 (2130 行受影響)
表 'Contacts_index'。掃描計數 1,邏輯讀取 14 次
索引影響 IO從568次降低到14次
評註 覆蓋索引對性能提升巨大,如果沒有索引,需要掃描整張表來找到所需數據,“2130 行”意味着以S開頭的姓還是挺多的,佔到整個Contact表的10%
表2.1 運行覆蓋查詢後的執行結果

測試非覆蓋查詢
接下來,和上面語句類似,只是執行語句中包含的列更多了,查詢的執行信息如表2.2所示

SQL語句 SELECT *
FROM dbo.Contacts – 分別執行在Contacts_noindex表和Contacts_index表上


WHERE LastName LIKE 'S%'
沒有非聚集索引 (2130 行受影響)
表 'Contacts_noindex'。掃描計數 1,邏輯讀取 568 次
有非聚集索引 (2130 行受影響)
表 'Contacts_index'。掃描計數 1,邏輯讀取 568 次
索引影響 完全沒有影響
評註 索引在查詢執行期間完全沒有被用到!
因爲使用書籤查找的成本遠遠高於整表掃描,因此SQL Server決定使用整表掃描.
表2.1 運行非覆蓋查詢後的執行結果

測試非覆蓋索引,但是選擇更少的列
SQL語句 SELECT *
FROM dbo.Contacts – 分別執行在Contacts_noindex表和Contacts_index表上

WHERE LastName LIKE 'Ste%'
沒有非聚集索引 (107行受影響)
表 'Contacts_noindex'。掃描計數 1,邏輯讀取 568 次
有非聚集索引 (107行受影響)
表 'Contacts_index'。掃描計數 1,邏輯讀取 111 次
索引影響 IO由568降低到111
評註 SQL Server訪問了107條索引內連續的”Ste”條目。通過每個索引條目中的書籤找到對應原表中的行,原表中的行並不連續。

非聚集索引減少了這個查詢的IO,但是根據測試結果可以看出,帶來的好處並不如覆蓋查詢,尤其是這條查詢還需要很多IO來獲取原表中的行。

你可能會認爲107個索引條目加上107行應該是107+107個IO讀,但僅僅111個IO讀的原因會在後續文章中提到,你現在只要知道只有少部分讀用於訪問索引條目,大部分IO讀都用在了訪問原表。

根據表2.2,也就是那個返回2130行數據的查詢,是不能夠從索引中獲益的,而這條索引只讀取107行,是可以從索引中獲益的,你也許會有疑問“那SQL Server如果知道索引是否能夠給查詢帶來增益呢”,這部分我們會在後續文章中講解。
表2.3 執行返回更少的數據的非覆蓋查詢後的執行結果

測試聚合覆蓋查詢
我們最後的例子是聚合查詢,也就是查詢中使用了聚合。下面的查詢根據LastName和FirstName進行分組來找到姓名完全相同的人.
部分查詢結果如下:
Steel Merrill 1
Steele Joan 1
Steele Laura 2
Steelman Shanay 1
Steen Heidi 2
Stefani Stefano 1
Steiner Alan 1 查詢執行的詳細信息見表2.4
SQL語句 SELECT LastName, FirstName, COUNT(*) as 'Contacts'
FROM dbo.Contacts – 分別執行在Contacts_noindex表和Contacts_index表上
WHERE LastName LIKE 'Ste%'
GROUP BY LastName, FirstName
沒有非聚集索引 (104行受影響)
表 'Contacts_noindex'。掃描計數 1,邏輯讀取 568 次
有非聚集索引 (104行受影響)
表 'Contacts_index'。掃描計數 1,邏輯讀取 4 次
索引影響 IO由568降低到4
評註 查詢所需的所有信息讀包含在索引中,並且這些條目在索引中還是按順序存放,所有以Ste開頭的條目都在索引中按順序進行存放。查詢按FirstName和LastName的值進行了聚合分組。

既不需要訪問表,也不需要進行排序操作,還是那句話,覆蓋索引性能提升巨大。
表2.4 覆蓋聚集查詢的執行結果

測試非覆蓋聚合查詢
我們在上面查詢的基礎上加入索引中不包含的列,得到了表2.5中的測試數據
SQL語句 SELECT LastName, FirstName, MiddleName, COUNT(*) as 'Contacts'
FROM dbo.Contacts --分別執行在Contacts_noindex表和Contacts_index表上
WHERE LastName LIKE 'Ste%'
GROUP BY LastName, FirstName, MiddleName
沒有非聚集索引 (105行受影響)
表 'Contacts_noindex'。掃描計數 1,邏輯讀取 568 次
有非聚集索引 (105行受影響)
表 'Contacts_index'。掃描計數 1,邏輯讀取 111 次
索引影響 IO由568次降爲111次,和前面非覆蓋查詢的性能一致。
備註 比如像使用內存和TempDB的中間工作過程有時候不在統計信息所包含的範圍之內,實際上,索引帶來的好處很多時候要比統計信息顯示的還要多。
表2.5 運行非覆蓋聚合查詢後的執行結果

總結
我們知道非聚集索引有如下特點,非聚集索引是









  • 實體的有序集合
  • 每一行都有一個對應所在表的入口指針
  • 包含索引鍵和書籤
  • 由用戶創建
  • 由SQL Server維護
  • 被SQL Server用來減少查詢所付出的代價

目前爲止,文中所演示的查詢既可以僅僅索引獲取數據,也可以僅僅通過表獲取,又或者是二者結合。
當一個查詢被傳到數據引擎時,SQL Server可以通過三種路徑獲取數據來滿足這個查詢。









  • 不需要訪問表僅需要訪問索引本身,這種情況必須是索引覆蓋了請求所包含的列
  • 使用索引鍵值去訪問非聚集索引,然後使用書籤訪問非聚集索引所在表
  • 無視非聚集索引掃描基本表來獲取數據

通常情況下,第一種方式是最理想的,第二種方式好於第三種。在接下的文章中我會講述如何提高索引覆蓋的概率以及如何知道指定非覆蓋查詢如何從非聚集索引獲益。但是這需要對索引的內部的結構有更深入的瞭解,這已經超越了本篇文章的內容。

在更深入的瞭解索引的內部結構之前,讓我們首先介紹另一種SQL Server的索引,也就是聚集索引,這將會在本系列的第三篇文章中進行介紹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章