一、書籤查詢的概念

  書籤可以幫助SQL Server快速從非聚集索引條目導向到對應的行,其實這東西幾句話我就能說明白。

  如果表有聚集索引(區段結構),那麼書籤就是從非聚集索引找到聚集索引後,利用聚集索引定位到資料。此處的書籤就是聚集索引。如果表沒有聚集索引(堆結構)。那麼掃描非聚集索引後,通過RID定位到資料,那麼此處書籤就是RID。

  所謂的書籤查詢,就是通過聚集索引,然後利用聚集索引或RID定位到資料。

  不論表示堆結構還是區段結構,資料的存放都是資料庫檔案的某檔案->某頁->某行,因此定位資料的檔案組合起來就是
檔案號:頁號:行號。這三個數字就是RID。如檔案1的第77頁的第12行的RID就是1:77:12。

  堆結構與區段結構不同,通常堆上的行不會改變位置,一旦他們被插入某個頁中,他們就會一直在那個位置。在堆上的行很少移動,如果行被移動的話,他們會在原來的位置留下指向其移動到的新位置的指標。而區段結構的行,是可以移動的,在新增資料或整理索引時,都可以會被移動位置。

  因為在堆上的行很少移動,所以RID就可以唯一標識某一行,RID的值不僅僅不變,RID所表示的行的物理位置也不會變,這使得RID的值更適宜作為書籤。這也是為什麼SQL Server在堆上建立的非聚集索引的書籤都使用RID。

  1、堆上的非聚集索引:基於RID的書籤

CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate   --主鍵不是聚集索引,沒有聚集索引
ON
Sales.SalesOrderDetail(ProductID, ModifiedDate)
INCLUDE (OrderQty,
UnitPrice, LineTotal)

  部分資料順序:

  

  注意到以上資料是無序的。

  上面建立的非聚集索引因為使用了RID作為書籤,直接指向對應行所在的物理位置,因此效率不錯。雖然RID值用於鍵查詢非常高效,但書籤中包含的值與具體的使用者資料無關。

  2、在聚集索引上的非聚集索引:基於聚集鍵的書籤

  如果表示基於聚集索引的,則表內資料可以在表移動。因此,對於聚集索引來說,RID並不能一直不變的定位一個相同的行。因此必須用另外的方法定位行,這個方法就是使用聚集索引的索引鍵。
  使用聚集索引鍵作為書籤可以使得當資料在頁中的行改變時,不需要非聚集索引的書籤的值進行變動,因此非聚集索引的鍵就可以用於去找底層表的資料,即根據書籤取資料不再基於物理位置,而是基於聚集索引查詢。

  
  以聚集索引鍵作為非聚集索引的書籤最好要聚集索引鍵滿足如下標準:
  索引應該具有唯一性:每一個索引條目書籤都應該使得書籤可以通過聚集索引的鍵值唯一的確認表中的一行,如果你建立的聚集索引鍵值不唯一,SQL Server將會為有重複鍵值的每一行自動加上一個叫uniquifier的東西使得每一行唯一。這個uniquifier對客戶端是透明的。對於是否可以允許聚集索引鍵重複,要考慮以下兩點:

  •   生成uniquifier增加SQL Server插入操作的額外負擔,在插入時SQL Server還需要判斷插入的值在表中是否唯一,如果不唯一生成uniquifier值再進行插入。
  •   uniquifier本身對業務資料來說是沒有意義的,但是這個uniquifier本身不僅僅需要佔用聚集索引鍵的空間,還同時佔用非聚集索引書籤的空間

  索引鍵應該短:索引鍵所佔的位元組數應該短.因為這個鍵還會佔用非聚集索引書籤的空間。比如Contact表中以Last name / first name / middle name / street組合作為索引鍵看上去不錯,但如果表中存在多個非聚集索引的話情況就有些微妙了。n個非聚集索引使得Last name / first name / middle name / street這些欄位被儲存在n+1個位置。

  索引鍵最好不要變動:也就是索引鍵的值最好不要變動。對於聚集索引鍵的修改會使得基於這個聚集索引的所有非聚集索引同樣進行修改。所以對於聚集索引的一次update會造成n個非聚集索引書籤的update+1個聚集索引鍵值本身的update。

  下面以一個示例來幫助理解書籤查詢:

  假設資料庫有一張表如下:

  

  我們再Name列建一個非聚集索引,然後執行下面的語句:

  

  從執行計劃我們可以看到,因為Age列並不在非聚集索引中,所以SQL Server通過“鍵查詢”引導到聚集表獲取資料,這就是書籤查詢。

  書籤查詢的目的,就是為了從非聚集索引導航到基本表獲取非聚集索引中並未包含的資訊。

二、書籤查詢的缺點

  書籤查詢要求訪問索引頁面之外的資料頁面,訪問兩組頁面增加了查詢邏輯讀操作次數。而且,如果頁面不在記憶體中,書籤查詢可能需要在磁碟上一個隨機I/O操作來從索引頁面跳轉到資料頁面,還需要必要的CPU能力來彙集這一資料並執行必要的操作。這是因為對於大的表,索引頁面和對應的資料頁面通常在磁碟上並不臨近。

  如果需要增加邏輯讀操作或者開銷較大的物理讀操作使書籤查詢的資料檢索操作開銷相當大,這個開銷因素是非聚集索引更適合於返回較小的資料行數的原因。隨著查詢檢索的行數增加,書籤查詢的開銷將變得無法接受。

  為了理解書籤查詢隨著檢索行數增加而使feu聚集索引無效,下面來看一個例項:

  還是那張Person表,一萬資料。這次,我把索引建在Id列,Id列的唯一性是1,因為原來Id列是做主鍵+聚集索引的,但被我刪掉了。

  我們來看看下面兩個查詢的執行計劃,

  返回100條:

  

  返回300條:

  

  我們看到,當要求返回300條資料的時候,SQL Server就不在使用Id列上的非聚集索引,而是直接進行表掃描了。因為SQL Server認為執行300次書籤查詢還不如直接對一張1萬條記錄的表進行全表掃描。

  由上面的例項可以得出結論,返回大的結果集將增加書籤查詢的開銷,甚至低於表掃描。因此在返回較大結果集的情況下,必須考慮避免書籤查詢的可能性。

三、書籤查詢的起因

  書籤查詢可能是一個開銷較大的操作,所以應該分析查詢計劃,在執行計劃中選擇一個關鍵字查詢步驟的原因。可能發現可以通過在非聚集索引鍵中包含丟失的行,或者作為索引頁面級別上的包含列來避免書籤查詢,從而避免與書籤查詢相關的開銷。

  從上面的例項,我們可以提出觀點:如果查詢的各部分(不只是選擇列表)中引用的列不都包含在使用的非聚集索引中,就會發生書籤查詢操作。

  下面介紹一個技巧,我們點選某一個執行計劃的圖示之後,就能在右側的屬性資訊欄裡獲取到相關的執行資訊。例如,輸出列表就是本執行計劃的要返回的列。

  

四、避免書籤查詢的方法

  因為書籤查詢的相對開銷可能非常高,所以應該儘可能嘗試擺脫書籤查詢操作。下面給出一下方案。

  1、使用聚集索引

  對於聚集索引,索引的葉子頁面和表的資料頁面相同。因此,當讀取聚集索引鍵列的值時,資料引擎可以讀取其他列的值而不需要任何導航。例如前面的區間資料查詢的操作,SQL Server通過B樹結構進行查詢是非常快速的。

  把非聚集索引轉換為一個聚集索引說起來很簡單。但是,這個例子和大部分可能遇到的情況下,這不可能做到,因為表已經有了一個聚集索引。這個表的聚集索引恰好是主鍵。必須解除安裝掉所有的外來鍵約束,解除安裝並且重建為一個非聚集索引。這不僅要考慮所涉及的工作,還可能嚴重地影響依賴於現有聚集索引的其他查詢。

  2、使用覆蓋索引

  為了理解覆蓋索引是如何避免書籤查詢,我們還是對於Person來執行如下兩個查詢:

  

  下面修改索引增加Name列。

  

  由於非聚集索引上已經有了需要查詢的Id和Name列的資料,所以不在需要書籤查詢定位到基本表。

  3、使用索引連線

  如果覆蓋索引變得非常寬,那麼可能要考慮索引連線技術。索引連線技術使用兩個或更多索引之間的一個索引交叉來完全覆蓋一個查詢。因為索引連線技術需要訪問多餘一個索引,它必須在所有索引連線中使用的索引上執行邏輯讀。因此,索引連線需要比覆蓋索引更高的邏輯讀數量。但是,因為索引連線所用的多個窄索引能夠比寬的覆蓋索引服務更多的查詢。所以索引連線也可以作為避免書籤查詢的一種技術來考慮。

  我們來看下面的例項:

  

  留意到,上面的例子我們建立了兩個非聚集索引,一個在 Id列,一個在Name列。但是我們的查詢需要同時返回Id列和Name列。而這兩個非聚集索引都不完全包含要返回列。這個時候,雜湊匹配目的就是通過定位到索引,而不用定位到基本表就能夠獲得我們所需要的全部資料,這樣索引連線就避免了書籤查詢。