真正了解查詢計劃和發現與計劃有關的問題,需要對構成這些計劃的查詢運算符有深入的了解.總而言之,有許多運算符值得探讨.
掃描與查找
掃描與查找操作均是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次的随機讀要比