1、前言數據庫
不少數據庫系統性能不理想是由於系統沒有通過總體優化,存在大量性能低下的SQL 語句。這類SQL語句性能很差的首要緣由是缺少高效的索引。沒有索引除了致使語句自己運行速度慢外,更是致使大量的磁盤讀寫操做,使得整個系統性能都受之影響而變差。解決這類系統的首要辦法是優化這些沒有索引或索引不夠好的SQL語句。工具
本文討論和索引相關的有關內容,以及經過分析語句的執行計劃來說述如何應用索引技術來優化SQL 語句。經過分析執行計劃,讀者能夠檢查索引是否有用,以及如何建立高效的索引。本文對數據庫管理人員以及數據庫系統開發人員都有必定參考意義。性能
若是讀者不知道應該優化數據庫系統的哪些SQL語句,那麼建議讀者參考筆者的另一篇文章,《應用Profiler優化SQL Server數據庫系統》。那篇文章介紹如何利用Profiler和Read80trace工具找出數據庫系統中的關鍵的和頻繁運行的SQL語句,你能夠把精力花在這些最值得優化的SQL語句上面。優化
2、建立索引的關鍵索引
優化SQL語句的關鍵是儘量減小語句的logical reads。這裏說的logical reads是指語句執行時須要訪問的單位爲8K的數據頁總數。logical reads 越少,其須要的內存和CPU時間也就越少,語句執行速度就越快。不言而喻,索引的最大好處是它能夠極大減小SQL語句的logical reads數目,從而極大減小語句的執行時間。建立索引的關鍵是索引要可以大大減小語句的logical reads。一個索引好很差,主要看它減小的logical reads多很少。ip
運行set statistics io命令能夠獲得SQL語句的logical reads信息。舉例以下:內存
在Query Analyzer 中運行以下的命令:開發
/***** Script 1 *****************************/get
set statistics io onio
select au_id,au_lname ,au_fname
from pubs..authors where au_lname ='Green'
set statistics io on
/********************************************/
輸出結果以下:
au_id au_lname au_fname
----------- ---------------------------------------- --------------------
213-46-8915 Green Marjorie
(1 row(s) affected)
Table 'authors'. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0.
上面的logical reads 1就是指該Select語句的邏輯讀總數是1。Logical reads 越少越好。若是Logical reads很大,而返回的行數不多,也即二者相差較大,那麼每每意味者語句須要優化。好比語句沒有索引,或索引不夠好等。注意Logical reads和後面的physical reads的區別。Logical reads中包含該語句從內存數據緩衝區中訪問的頁數和從物理磁盤讀取的頁數。而physical reads表示那些沒有駐留在內存緩衝區中須要從磁盤讀取的數據頁。Read-ahead reads是SQL Server爲了提升性能而產生的預讀。預讀可能會多讀取一些數據。 優化的時候咱們主要關注Logical Reads就能夠了。注意若是physical Reads或Read-ahead reads很大,那麼每每意味着語句的執行時間(duration)裏面會有一部分耗費在等待物理磁盤IO上。
2、單字段索引,組合索引和覆蓋索引
顧名思義,單字段索引是指只有一個字段的索引,而組合索引指有多個字段構成的索引。
下面的例子講述建立這些索引的一些技巧,以及如何結合執行計劃判斷SQL語句是否利用了索引。
1. 對出如今where子句中的字段加索引
先運行以下的語句建立示例所須要的表:
/**************Script 2************************************/
use tempdb
go
if exists (select * from dbo.sysobjects where id = object_id(N'[tbl1]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [tbl1]
GO
create table tbl1
(學生號 int,學生姓名 varchar(20),性別 char(2), 年齡 int,入學時間 datetime,備註 char(500))
go
declare @i int
set @i=0
declare @j int
set @j=0
while @i<5000
begin
if (rand()*10>3) set @j=1 else set @j=0
insert into tbl1 values(@i,
char( rand()*10+100)+char( rand()*5+50)+char( rand()*3+100)+char( rand()*6+80),
@j, 20+rand()*10,convert(varchar(20), getdate()-rand()*3000,112),
char( rand()*9+100)+char( rand()*4+50)+char( rand()*2+130)+char( rand()*5+70))
set @i=@i+1
end
/**************************************************/
而後咱們看以下的語句應該如何建立索引:
/********Script 3**********************************/
set statistics profile on
set statistics io on
go
select 學生姓名, 入學時間 from tbl1 where 學生號=972
go
set statistics profile off
set statistics io off
go
/****************************************************/
注意上面的set statistics profile命令將輸出語句的執行計劃。也許你會問,爲何不用SET SHOWPLAN_ALL呢?使用SET SHOWPLAN_ALL也是能夠的。不過set statistics profile輸出的是SQL 語句的運行時候真正使用的執行計劃,而SET SHOWPLAN_ALL輸出的是預計(Estimate)的執行計劃。使用SET SHOWPLAN_ALL是後面的語句並不會真正運行。
上面script輸出結果(部分)以下:
學生姓名 入學時間
-------------------- ------------------------------------------------------
g4eQ 2005-05-29 00:00:00.000
(1 row(s) affected)
Table 'tbl1'.Scan count 1,logical reads 385, physical reads 0,read-ahead reads 0.
Rows Executes StmtText
------------------------------------------------------------------------------
1 1 SELECT [學生姓名]=[學生姓名],[入學時間]=[入學時間] FROM [tbl1]
1 1 |--Table Scan(OBJECT:([tempdb].[dbo].[tbl1]), WHERE:([tbl1].
(2 row(s) affected)
從上面輸出結果能夠看到,這條語句執行時候使用了Table Scan,也就是對整個表進行了全表掃描。全表掃描的性能一般是不好的,要儘可能避免。若是上面的select語句是數據庫系統常常運行的關鍵語句, 那麼應該對它建立相應的索引。建立索引的技巧之一是對常常出如今where條件中的字段建立索引。因此對上面的select語句,應該在學生號字段上創建單字段索引idx_學生號:
create nonclustered index idx_學生號 on tbl1(學生號)
而後再運行Script 3,部分結果以下:
Table 'tbl1'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0.
Rows Executes StmtText
---------------------------------------------------------------------------------------------
1 1 SELECT [學生姓名]=[學生姓名],[入學時間]=[入學時間] FROM [tbl1] WHERE [學生號]=@
1 1 |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[tbl1]))
1 1 |--Index Seek(OBJECT:([tempdb].[dbo].[tbl1].[idx_學生號]), SEEK:([tbl1].
上面的結果顯示咱們剛剛建立的idx_學生號這個索引確實被使用到了。語句的logical reads極大減小,從沒有索引前的385減小到3,Table Scan也變成了Index Seek,性能極大提升。從上面的例子能夠知道,若是你在執行計劃中看到Table Scan或彙集索引的Index Scan(彙集索引的Index Scan至關於Table Scan), 並且對應的logical reads至關大,那麼就要設法使之變成Index seek。設法避免Table scan或Index scan是優化SQL 語句使用的經常使用技巧。一般Index Seek須要的logical reads比前二者要少得多。
2.組合索引
若是where語句中有多個字段,那麼能夠考慮建立組合索引。例子以下:
/*****Script 4******************************************/
set statistics profile on
set statistics io on
go
select學生姓名, 入學時間 from tbl1
where 入學時間>='20050301' and 入學時間<'20050305' and 年齡>24
go
set statistics profile off
set statistics io off
go
/*******************************************************/
爲了提升該語句的性能,能夠在入學時間和年齡上創建一個組合索引以下:
create nonclustered index idx_入學時間年齡 on tbl1(入學時間,年齡)
你也許會問,若是把入學時間和年齡字段換個位置創建以下的組合索引如何?
create nonclustered index idx_年齡入學時間 on tbl1(年齡,入學時間)
這個索引沒有前面的好。分析這兩個字段的惟一性:
select count(*) from tbl1 group by 入學時間
select count(*) from tbl1 group by 年齡
部分輸出結果以下:
distinct_value_of 入學時間
(2426 row(s) affected)
distinct_value_of 年齡
(10 row(s) affected)
結果顯示入學時間字段有2426個惟一值,而年齡字段只有10個。也就是說入學時間字段的惟一性比年齡字段高得多。對於上面的兩個索引分別運行Script 4,你會發現對第一個索引語句的logical reads是8 而第二個索引致使的logical reads爲16,相差了一倍。若是表很大那麼性能的差別可想而知。因此,組合索引中字段的順序是很是重要的,越是惟一的字段越是要靠前。另外,不管是組合索引仍是單個列的索引,儘可能不要選擇那些惟一性很低的字段。好比說,在只有兩個值0和1的字段上創建索引沒有多大意義。
有時候你要決定爲每一個相關字段單獨創建索引仍是創建一個組合索引。好比說若是下面的語句常常執行:
Select c1, c2,c3 from tblname where c1='abc' and c2=3
Select c1, c3 from tblname where c1='b'
Select c1, c2 from tblname where c2=10
應該如何創建索引呢?這取決於各語句的比例。若是大部分語句老是根據c1和c2查詢,那麼一個組合索引(c1+c2)或者一個覆蓋索引是很是有用的,而後多加一個單獨對c3建立的索引。反之,若是第一個語句運行次數很是少,大部分語句是後面兩種,那麼固然要對c1和c2分別創建索引。你也許會問,對第一種語句,分別對c1和c2創建索引能夠嗎?能夠。對某些語句SQL Server 可能會分別使用兩個索引(即索引交叉技術)查詢數據而後取其交集獲得結果。但有時候SQL Server 未必會使用你創建的所有的單字段索引。因此若是對單字段進行索引,建議使用set statistics profile來驗證索引確實被充分使用。logical reads越少的索引越好。
3.覆蓋索引
對於script 4中的select語句,有沒有更好的索引呢?有的。那就是使用覆蓋索引(covered index)。覆蓋索引可以使得語句不須要訪問表僅僅訪問索引就可以獲得全部須要的數據。由於彙集索引葉子節點就是數據因此無所謂覆蓋與否,因此覆蓋索引主要是針對非彙集索引而言。不知你們注意到沒有,咱們前面討論的執行計劃中除了index seek外,還有一個Bookmark Lookup關鍵字。Bookmark Lookup表示語句在訪問索引後還須要對錶進行額外的Bookmark Lookup操做才能獲得數據。也就是說爲獲得一行數據起碼有兩次IO,一次訪問索引,一次訪問基本表。若是語句返回的行數不少,那麼Bookmark Lookup操做的開銷是很大的。覆蓋索引可以避免昂貴的Bookmark Lookup操做,減小IO的次數,提升語句的性能。
覆蓋索引須要包含select子句和WHERE子句中出現的全部字段。Where語句中的字段在前面,select中的在後面。就script 5中的select語句而言,覆蓋索引以下:
create nonclustered index idx_covered on tbl1(入��時間,年齡,學生姓名)
而後再運行script 4,輸出結果以下:
Table 'tbl1'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0.
Rows Executes StmtText
------------------------------------------------------------------------------------------------------
6 1 SELECT [學生姓名]=[學生姓名],[入學時間]=[入學時間] FROM [tbl1] WHERE [入學時間]>=@1 AND [入學時間]<@2
6 1 |--Index Seek(OBJECT:([tempdb].[dbo].[tbl1].[idx_covered]), SEEK:(([tbl1].[入學時間], [tbl1].[年齡])
比較一下上面的logical reads,是大大減小了。Bookmark Lookup操做也消失了。因此建立覆蓋索引是減小logical reads提高語句性能的很是有用的優化技巧。
實際上索引的建立原則是比較複雜的。有時候你沒法在索引中包含了Where子句中全部的字段。在考慮索引是否應該包含一個字段時,應考慮該字段在語句中的做用。好比說若是常常以某個字段做爲where條件做精確匹配返回不多的行,那麼就絕對值得爲這個字段創建索引。再好比說,對那些很是惟一的字段如主鍵和外鍵,常常出如今group by,order by中的字段等等都值得建立索引。因篇幅有限,這裏再也不進行展開了。SQL Server的聯機手冊中有很好的相關內容,請讀者自行參考。