天天看點

索引掃描、查找、書簽查詢、覆寫查詢示例介紹

         真正了解查詢計劃和發現與計劃有關的問題,需要對構成這些計劃的查詢運算符有深入的了解.總而言之,有許多運算符值得探讨.

掃描與查找

         掃描與查找操作均是SQL Server從表或索引中讀取資料采用的疊代器,這些也是SQL Server支援的最基本的運算.幾乎在每一個查詢計劃中都可以找到,是以了解它們的不同是很重要的,掃描是在整張表上進行處理,而索引是在整個頁級上進行處理,而查找則傳回特定謂詞上一個或多個範圍内的資料行.

         下面讓我們看一個掃描的例子(這裡使用Northwind資料庫)

         SELECT [OrderId] FROM [Orders] WHERE [RequiredDate] = '1998-03-26'

         在Orders表中,并不存在對RequiredDate列的索引,是以,SQL Server必須讀取Orders表的每一行來估計每一行的RequiredDate謂詞,如果滿足該謂詞條件(即找到包含’1998-03-26’的記錄),則傳回該行資料.

         為了最大化提升性能,SQL Server盡可能地使用掃描疊代器來估計該謂詞,然而,如果該謂詞過于複雜或開銷過大,SQL Server或許使用别的篩選疊代器來估計.以下是WHERE關鍵字中的文本計劃的過程:

|--Clustered Index Scan(OBJECT:([Orders].[PK_Orders]),

   WHERE:([Orders].[RequiredDate]='1998-03-26'))

下圖描述了該操作的流程圖:

由于掃描表的每一行資料,不論滿足與否,是以,其查詢開銷對表中的總記錄數是均衡的,當表中的資料很少或滿足謂詞的行比較多時,采用掃描操作有效,如果表中資料量比較大或滿足謂詞的行較少時,使用掃描将讀取更多的頁面或執行更多的I/O操作來擷取資料,這顯而不是最有效的方法.

         下面讓我們看一個關于索引查找的例子,下面的例子在OrderdDate列上建立了索引:

    SELECT [OrderId] FROM [Orders] WHERE [OrderDate] = '1998-02-26'      

          這次SQL Server能夠使用索引查找來直接找到滿足謂詞的那些記錄行,這裡稱該謂詞為"查找"謂詞.大多數情況下,SQL Server并不顯式地估計"查找"謂詞,而索引確定了"查找"操作僅傳回滿足的資料行,以下是"查找"謂詞的文本計劃:

|--Index Seek(OBJECT:([Orders].[OrderDate]),

   SEEK:([Orders].[OrderDate]=CONVERT_IMPLICIT(datetime,[@1],0)) ORDERED FORWARD)

注意:SQL Server自動使用@1參數替換查詢文本中的參數

由此看來,查找僅掃描滿足該謂詞的資料頁,其查詢開銷顯然要比表中總記錄數的開銷低,是以,對于高選擇度的查詢謂詞操作,查找通常是最有效的政策.也就是說,對于估計大表中的資料時,使用查找謂詞是比較有效率的.

SQL Server将掃描與查找進行區分,如同将在堆(無聚集索引的對象)上掃描,聚集索引上的掃描,非聚集索引上的掃描進行分區.下表說明了這些出現在的查詢計劃中的掃描與查找運算.

掃描 查找
表掃描
聚集索引 聚集索引找描 聚集索引查找
非聚集索引 索引掃描 索引查找

可查找的謂詞與覆寫列

         SQL Server在執行索引查找之前,它需要确定索引鍵是否滿足查詢中的謂詞,我們稱該謂詞為"可查找的謂詞",SQL Server必須确定該索引是否包含或"覆寫"查詢中引用的列集合.下面描述了如何确定哪個謂詞是可查找的,哪個謂詞不是可查找的,哪些列需要索引覆寫.

單列索引

         在單列索引上判斷謂詞是否是可查找的是很容易的,SQL Server使用單列索引來響應多數簡單的比較(包括相等和不等(大于,小于等))或者更複雜的表達式,如在列上運算的函數和LIKE %謂詞,這些運算符将阻止SQL Server使用索引查找.

例如,假設我們在Col1列上建立了單列索引,可以在以下謂詞上進行索引查找:

  • Ø  [Col1] = 3.14
  • Ø  [Col1] > 100
  • Ø  [Col1] BETWEEN 0 AND 99
  • Ø  [Col1] LIKE 'abc%'
  • Ø  [Col1] IN (2, 3, 5, 7)

然頁,在以下謂詞上将不能使用索引查找:

  • Ø  ABS([Col1]) = 1
  • Ø  [Col1] + 1 = 9
  • Ø  [Col1] LIKE '%abc'

下面我通過一些例子來介紹單列索引:

首先建立一些架構對象:

create table person

(id int, last_name varchar(30), first_name varchar(30))

create unique clustered index person_id

on person (id)

create index person_name

on person (last_name, first_name)

以下是三個查詢及其各自的文本查詢計劃,第一個查詢在person_name索引上進行查找,第二個查詢首先在第一個鍵列上進行索引查找,然後使用residual謂詞來估計first_name,第三個查詢不能使用索引查找,而是使用了索引掃描來處理residual謂詞.

select id from person where last_name = 'Doe' and first_name = 'John'

  |--Index Seek(OBJECT:([person].[person_name]), SEEK:([person].[last_name]='Doe' AND [person].[first_name]='John'))

select id from person where last_name > 'Doe' and first_name = 'John'

  |--Index Seek(OBJECT:([person].[person_name]), SEEK:([person].[last_name] > 'Doe'),  WHERE:([person].[first_name]='John'))

select id from person where last_name like '%oe' and first_name = 'John'

  |--Index Scan(OBJECT:([person].[person_name]),  WHERE:([person].[first_name]='John' AND [person].[last_name] like '%oe'))

上面三條查詢的圖形查詢計劃:

複合索引

         複合索引或多列索引,與單列索引相比,略顯得複雜,對于複合索引,比較關心的是鍵列的次序,它決定了索引的排序,同時對SQL Server使用該索引來進行估計的查找謂詞集合有一定的影響.

         用一種簡單的方法來說明這一問題就是使用電話本,電話本可能包括last_name,first_name列的索引.如果電話本的内容是按照last_name進行排序,要查找某個人的電話,隻需知道last_name就可以輕松地找到該人,然而,如果我們僅知道該人的first_name,要找出符合該名的電話清單就是那麼容易了,這就需要在first_name上進行排序的另一個電話本.

    例如,在Col1和Col2列上有一個兩列的索引,可以使用該索引來查找單列索引上的任何一個謂詞,可以在以下謂詞上進行查找:

  • Ø  [Col1] = 3.14 AND [Col2] = 'pi'
  • Ø  [Col1] = 'xyzzy' AND [Col2] <= 0

對于使用索引來滿足col1列上的謂詞,并非是col2列,在這些情況中,需要使用剩餘謂詞來查找:

  • Ø  [Col1] > 100 AND [Col2] > 100
  • Ø  [Col1] LIKE 'abc%' AND [Col2] = 2

不能使用索引來查找不在col1列上的謂詞查找,則必須使用不同的索引來查找(也就是說以col2為第一個鍵列)或使用謂詞掃描.

  • Ø  [Col2] = 0
  • Ø  [Col1] + 1 = 9 AND [Col2] BETWEEN 1 AND 9
  • Ø  [Col1] LIKE '%abc' AND [Col2] IN (1, 3, 5)

辨別索引鍵

在多數情況下,索引鍵通常是在CREATE INDEX語句中指定的列集合,當在含有聚集索引的表上建立非唯一的非聚集索引時,如果該索引并未顯式說明非聚集索引鍵的部分時,需要将聚集索引鍵添加到非聚集索引鍵上.正如可以顯式指定他們來查找這些隐式的鍵列一樣.

覆寫列

         對于堆或聚集索引的一張表,也叫"基表",包含(覆寫)了表的所有列,換句話說,非聚集索引隻包含(覆寫)了表中的一小部分列,通過在非聚集索引來限制列的集合,SQL Server可以在每一頁儲存更多的行,這顯然節約了空間,提高查找與掃描的效率,降低了I/O的操作數和頁面數.然而,對于索引的掃描或查找來說,它隻能傳回該索引覆寫的那些列的記錄行.

         當建立覆寫列時,可以在非聚集索引上指定這些鍵列.如果基表是含有聚集索引的,位于該表上的每一個非聚集索引将覆寫聚集索引鍵,而不關心它們是否是非聚集索引鍵列的成員.在SQL Server 2005中,我們可以在非聚集索引上使用CREATE INDEX ...INCLUDE子語來添加額外的鍵列,注意:和索引鍵不同,INCLUDE中的列的次序并不重要.

下面我來通過一個示例說明,首先我們建立以下架構和對象:

         CREATE TABLE T_heap (a int, b int, c int, d int, e int, f int)

CREATE INDEX T_heap_a ON T_heap (a)      
CREATE INDEX T_heap_bc ON T_heap (b, c)      
CREATE INDEX T_heap_d ON T_heap (d) INCLUDE (e)      
CREATE UNIQUE INDEX T_heap_f ON T_heap (f)      
CREATE TABLE T_clu (a int, b int, c int, d int, e int, f int)      
CREATE UNIQUE CLUSTERED INDEX T_clu_a ON T_clu (a)      
CREATE INDEX T_clu_b ON T_clu (b)      
CREATE INDEX T_clu_ac ON T_clu (a, c)      
CREATE INDEX T_clu_d ON T_clu (d) INCLUDE (e)      
CREATE UNIQUE INDEX T_clu_f ON T_clu (f)      

下面列舉上面每一個索引的鍵列和覆寫列.

索引名稱 鍵列
T_heap_a a
T_heap_bc b,c
T_heap_d d d,e
T_heap_f f
T_clu_a a,b,c,d,e,f
T_clu_b b,a a,b
T_clu_ac a,c
T_clu_d d,a a,d,e
T_clu_f a,f

         注意:對于T_clu表中的每一個非聚集索引鍵列均包含一個聚集索引鍵(T_clu_f除外,它是一個唯一索引).T_clu_ac顯式包含了索引的第一個鍵,而其他的索引并無顯式包含a列.

         對于建立的列在實際的索引查找和書簽查詢是有何不同呢?

         下面我們來看一個例子:

     SELECT e from t_clu where b = 2      

         初看,這個查詢看起來符合索引查找的候選,然而,該索引并不覆寫列e,因而索引的掃描或查找并不能傳回e列的值,其解決方法是很簡單的,對于從非聚集索引中擷取的每一行,我們可以通過聚集索引來查詢e列的值,這種方法稱為"書簽查詢",書簽查詢是一個指向堆或聚集索引行的一個指針.在非聚集索引中存儲每一行的書簽,這樣每次在進行非聚集索引查詢時,總是由非聚集索引轉向基表中相對應的行.

書簽查詢

         在上面的例子中,我們了解了SQL Server如何使用索引查找來有效地擷取滿足謂詞上的資料,然而,我們也知道非聚集索引并不覆寫表的所有列.試想一下,若我們有這樣一個在非聚集索引鍵上的謂詞查詢:select查詢的列并未被索引覆寫,當SQL Server在非聚集索引上查找時,将會丢失一些需要的列,與之相反,如果在聚集索引或唯上執行掃描時,将擷取所有列,由于要掃描表的每一行,其操作顯然不是很有效.以下的查詢就是如此:

SELECT [OrderId], [CustomerId] FROM [Orders] WHERE [OrderDate] = '1998-02-26'      

上面的查詢與先前我們進行索引查找的用到的查詢一樣,唯一不同的是,這裡我們選擇了兩列:OrderID和CustomerID.非聚集索引OrderDate列僅覆寫OrderID列.

         SQL Server具有處理這一問題的方法,對于從非聚集索引中擷取的每一行,它可能查詢包含在聚集索引中的剩餘列(這裡是CustomerID),我們稱該操作為"書簽查詢".書簽查詢是一個指向堆或聚集索引行的指針.SQL Server在非聚集索引中存儲每一行的書簽,這樣一來,可以從非聚集索引直接轉向基表中相對應的記錄行.

         SQL Server 2000使用指定的疊代器來實作書簽查詢,通過文本計劃可以看出索引查找與書簽查詢疊代器:

|--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([Orders]))      
     |--Index Seek(OBJECT:([Orders].[OrderDate]),      
        SEEK:([Orders].[OrderDate]=Convert([@1])) ORDERED FORWARD)      

         下圖為SQL Server 2000中的圖形查詢計劃:

在SQL Server 2005中将使用嵌套循環聯接和聚集索引查找來實作,僅當基表含有聚集索引或RID查詢(基表是堆),SQL Server 2005中的查詢計劃與SQL Server 2000中的計劃有些不同,但邏輯上是相同的.聚集索引查找就是通過LOOKUP關鍵來

實作的一種書簽查詢或通過屬性Lookup=”1”.以下是SQL Server 2005的圖形查詢計劃和文本查詢計劃:

|--Nested Loops(Inner Join, OUTER REFERENCES:([Orders].[OrderID]))      
       |--Index Seek(OBJECT:([Orders].[OrderDate]),      
          SEEK:([Orders].[OrderDate]='1998-02-26') ORDERED FORWARD)      
       |--Clustered Index Seek(OBJECT:([Orders].[PK_Orders]),      
         SEEK:([Orders].[OrderID]=[Orders].[OrderID]) LOOKUP ORDERED FORWARD)      

書簽查詢可以用在堆中,也可以用在聚集索引中,正如上所描述的,在SQL Server 2000中,堆上的書簽查詢與聚集索引

上的書簽查詢是相同的,而在SQL Server 2005中,堆上的書簽查詢仍舊使用一個嵌套循環聯接運算,代替了聚集索引查找

,SQL Server使用一個叫做RID查詢運算符.RID查詢運算符包括在堆上進行書簽查詢的查找謂詞,但是堆并不是一個索

引,RID查詢也不是一個索引查找.

索引示例介紹

         查詢優化器在從表中查詢資料時,需要選擇一個合适的通路模式,在決定使用哪一種索引,使用掃描還是查找,使用書簽

查詢時,查詢優化器要考慮許多因素,這些因素包括:

l   索引執行時,查找或掃描所需的I/O數

l   評估查詢中的索引鍵是否是最佳

l   謂詞的選擇性(也就是說,相對于表中總記錄數滿足謂詞的百分比)

l   索引是否覆寫所有列?

下面通過一個例子來介紹:

createtable T (a int, b int, c int, d int, x char(200))

createuniqueclusteredindex Ta on T(a)

createindex Tb on T(b)

createindex Tcd on T(c, d)

createindex Tdc on T(d, c)

         插入一些資料:

         setnocounton

declare @i int

set @i = 0

while @i < 100000

    begin

        insert T values(@i, @i, @i, @i, @i)

    set @i = @i + 1

    end

無WHERE條件

SELECT a,b FROM T,

該查詢不包含WHERE條件語句,而使用掃描,可是這裡有兩種索引可用:聚集索引(Ta)和非聚集索引(Tb),這兩個索

引均覆寫a和b兩列,另外,聚集索引也覆寫c和x列.由于x列是字元型,長度為200個字元,聚集索引的每一行

總寬度超過了200個位元組,對于每一個8KB的頁面,存儲的行數也不超過40行.而索引需要2500個頁來

存儲所有10萬行資料,與之相反的是,非聚集索引中每一行的總寬僅有8個位元組,加一些頭部資訊,每一頁可以

存儲上百行資料,索引則需要不到250頁來存儲所有的10萬行資料.通過掃描非聚集索引,當執行查詢時則需

要較少的I/O操作.因而使用的最佳計劃是:

|--Index Scan(OBJECT:([T].[Tb]))

         我們也可以使用sys.dm_db_index_physical_stats視圖來比較聚集索引與非聚集索引兩者所使用的頁數

   select index_id, page_count

from sys.dm_db_index_physical_stats

(DB_ID('northwind'),OBJECT_ID('T'),NULL,NULL,NULL)

執行上述查詢後,結果如下:

index_id    page_count

----------- --------------------

1             2858

2             174

3             223

4             223

從輸出結果可以看出,非聚集索引存儲行所使用的頁數明顯小于聚集索引使用的頁數.

當然我們也可以使用stats I/O和索引hints來比較聚集索引與非聚集索引的I/O數.

    setstatisticsioon

select a, b from T with(index(Ta))

表'T'。掃描計數1,邏輯讀取2872 次,實體讀取0 次,預讀0 次,lob 邏輯讀取0 次,lob 實體讀取0 次,lob 預讀0 次。

select a, b from T with(index(Tb))

表'T'。掃描計數1,邏輯讀取176 次,實體讀取0 次,預讀0 次,lob 邏輯讀取0 次,lob 實體讀取0 次,lob 預讀0 次。

         從stats I/O數可以看出,非聚集索引在擷取資料時,讀取較少的資料頁.

索引的選擇性

         select a from T

where c > 150 and c < 160 and d > 100 and d < 200

         此查詢有兩個不同的謂詞用于索引查找,可以使用位于c列上的非聚集索引Tcd,也可以使用位于d列上的非聚集索引Tdc.

查詢優化器通過檢視兩個謂詞的選擇性來确定使用哪一個索引,在c列上的謂詞選擇的行僅有9行,而在d列上則有99行,顯

然使用索引Tcd來評估位于d列上的residual謂詞比使用Tdc索引的I/O開銷要小得多.

         以下是該查詢的計劃:

           |--Index Seek(OBJECT:([T].[Tcd]), SEEK:([T].[c] > (150) AND [T].[c] < (160)), 

WHERE:([T].[d]>(100) AND [T].[d]<(200)) ORDERED FORWARD)

索引查找與索引掃描示例

    select a from T where a between 1001 and 9000

select a from T where a between 101 and 90000

其執I/O資訊如下:

表'T'。掃描計數1,邏輯讀取234 次,實體讀取0 次,預讀0 次,lob 邏輯讀取0 次,lob 實體讀取0 次,lob 預讀0 次。

如您所預料的,對于第一個查詢來說,查詢優化器在a列上選擇使用聚集索引來擷取資料,以下是其的查詢計劃:

|--Clustered Index Seek(OBJECT:([T].[Ta]),

SEEK:([T].[a] >= CONVERT_IMPLICIT(int,[@1],0) AND [T].[a] <= CONVERT_IMPLICIT(int,[@2],0)) ORDERED FORWARD)

注意:該計劃中的兩個參數是由自動參數化功能所生成的,當執行該計劃時,@1參數為1001,@2參數為9000.

對于第二個查詢來說,查詢優化器卻選擇了非聚集索引來掃描資料,以下其查詢計劃:

|--Index Scan(OBJECT:([T].[Tb]),  WHERE:([T].[a]>=(101) AND [T].[a]<=(90000)))

為什麼是這樣呢?注意第一個查詢選擇的記錄數有8千行(相對于10萬行資料而言),對于聚集索引來說,

選擇度為表的8%,約230個資料頁,而第二查詢選擇的記錄數有89000行,選擇度為表的約90%,

若使用聚集索引來讀取89000行資料時,則需要讀2500個資料頁.通過比較,非聚集索引僅需要讀取174個頁面,

查詢優化器選擇此計劃,大大減少了I/O操作.

帶書簽查詢的查詢與掃描示例

    select x from T where b between 101 and 200

select x from T where b between 1001 and 2000

         對于上述的兩個查詢而言,可以通過聚集索引直接掃描然後在列b上應用謂詞,或者使用非聚集索引Tb在列b上執行索引

查找,然後在聚集索引上執行書簽查詢來讀取滿足x列值的行.(注意:書簽查詢采用的I/O開銷比較大的方式是随機讀.)

對于查找的選擇度高的書簽查詢,則是值得的.

         以下是第一個包含書簽查詢的查詢計劃(僅需要讀取100行):

           |--Nested Loops(Inner Join, OUTER REFERENCES:([T].[a], [Expr1005]) ...)

       |--Index Seek(OBJECT:([T].[Tb]), SEEK:([T].[b] >= (101) AND [T].[b] <= (200)) ...)

       |--Clustered Index Seek(OBJECT:([T].[Ta]), SEEK:([T].[a]=[T].[a]) LOOKUP ...)

  而第二個查詢則讀取1000行,對于表而言,僅有1%.查詢優化器由此推出,執行1000次的随機讀要比