1. 程式人生 > >常用SQL語句書寫技巧-

常用SQL語句書寫技巧-

一、 建立索引

(一)建立“適當”的索引,是快速查詢的基礎。

索引(index)是除表之外另一重要的、使用者定義的儲存在物理介質上的資料結構。當根據索引碼的值搜尋資料時,索引提供了對資料的快速訪問。事實上,沒有索引,資料庫也能根據SELECT語句成功地檢索到結果,但隨著表變得越來越大,使用“適當”的索引的效果就越來越明顯。注意,在這句話中,我們用了 “適當”這個詞,這是因為,如果使用索引時不認真考慮其實現過程,索引既可以提高也會破壞資料庫的工作效能。
索引實際上是一種特殊的目錄,SQL SERVER提供了兩種索引:
? 聚集索引(clustered index,也稱聚類索引、簇集索引)
我們把這種正文內容本身就是一種按照一定規則排列的目錄稱為“聚集索引”。
例如:
漢語字典中按拼音查某一個字,就是使用“聚集索引”,實際上,你根本用不著查目錄,直接在字典正文裡找,就能很快找到需要的漢字(假設你知道發音)。
? 非聚集索引(nonclustered index,也稱非聚類索引、非簇集索引)
我們把目錄純粹是目錄,正文純粹是正文的排序方式稱為“非聚集索引”。
例如:
漢語字典中按部首查某一個字,部首目錄和正文一定要刻意的通過頁碼才能聯絡到一起,其順序不是天然一致的。

聚集索引與非聚集索引的最大區別就在於:聚集索引是對原資料表進行排序,因此只要符合索引條件,就能夠直接連續的讀取資料記錄,幾乎可以達到對資料表的零掃描;而非聚集索引僅僅只是另外建了一張索引表,取資料的時候,從索引表取得結果後,還需要到指標所指的資料行讀取相應資料,因此,在效能上,聚集索引會大大優於非聚集索引。
但是在一張表中,聚集索引只允許一個,是比較寶貴的,因此要儘可能的用於那些使用頻率最高的索引上。 另外,查詢時必需要用到索引的起始列,否則索引無效。另外,起始列也必需是使用頻繁的列,那樣的索引效能才會達到最優化。

(二)表:何時應使用聚集索引或非聚集索引

動作描述 使用聚集索引 使用非聚集索引
列經常被分組排序 ○ ○
返回某範圍內的資料 ○
一個或極少不同值
小數目的不同值 ○
大數目的不同值 ○
頻繁更新的列 ○
外來鍵列 ○ ○
主鍵列 ○ ○
頻繁修改索引列 ○

(三)索引建立的一些注意項

1、不要把聚集索引浪費在主鍵上,除非你只按主鍵查詢
雖然SQL SERVER預設是在主鍵上建立聚集索引的,但實際應用中,這樣做比較浪費。通常,我們會在每個表中都建立一個ID列,以區分每條資料,並且這個ID列是自動增大的,步長一般為1。此時,如果我們將這個列設為主鍵,SQL SERVER會將此列預設為聚集索引。這樣做有好處,就是可以讓您的資料在資料庫中按照ID進行物理排序,但這樣做實用價值不大。
從我們前面談到的聚集索引的定義我們可以看出,使用聚集索引的最大好處就是能夠根據查詢要求,迅速縮小查詢範圍,避免全表掃描。在實際應用中,因為ID號是自動生成的,我們並不知道每條記錄的ID號,所以我們很難在實踐中用ID號來進行查詢。這就使讓ID號這個主鍵作為聚集索引成為一種資源浪費。聚集索引相對與非聚集索引的優勢是很明顯的,而每個表中只能有一個聚集索引的規則,這使得聚集索引變得更加寶貴,應該用在其他查詢頻率高的欄位上。其次,讓每個 ID號都不同的欄位作為聚集索引也不符合“大數目的不同值情況下不應建立聚合索引”規則;當然,這種情況只是針對使用者經常修改記錄內容,特別是索引項的時候會負作用,但對於查詢速度並沒有影響。

2、索引的建立要根據實際應用需求來進行


並非是在任何欄位上簡單地建立索引就能提高查詢速度。聚集索引建立的規則大致是“既不能絕大多數都相同,又不能只有極少數相同”。舉個例子,在公文表的收發日期欄位上建立聚合索引是比較合適的。在政務系統中,我們每天都會收一些檔案,這些檔案的發文日期將會相同,在發文日期上建立聚合索引對效能的提升應該是相當大的。在群集索引下,資料物理上按順序存於資料頁上,重複值也排列在一起,因而在範圍查詢時,可以先找到這個範圍的起末點,且只在這個範圍內掃描資料頁,避免了大範圍掃描,提高了查詢速度。
另一個相反的例子:比如在僱員表的“性別”列上只有“男”與“女”兩個不同值,因此就完全沒必要建立索引。

3、在聚集索引中加入所有需要提高查詢速度的欄位,形成複合索引

根據一些實驗的結果,我們可以得出一些可供參考的結論:
? 僅用複合聚集索引的起始列作為查詢條件和同時用到複合聚集索引的全部列的查詢,速度是幾乎一樣的,甚至比後者還要快(在查詢結果集數目一樣的情況下);
? 僅用複合聚集索引的非起始列作為查詢條件的話,
這個索引是不起任何作用的。
? 複合聚集索引的所有列都用上,而且因為查詢條件嚴格,查詢結果少的話,會形成“索引覆蓋”,效能可以達到最優。
? 最重要的一點:無論是否經常使用複合聚合索引的其他列,其起始列一定要是使用最頻繁的列。

4.根據實踐得出的一些其他經驗,特定情況下有效
? 用聚合索引比用不是聚合索引的主鍵速度快;
? 用聚合索引比用一般的主鍵作order by速度快,特別是在小資料量情況;
? 使用聚合索引內的時間段,搜尋時間會按資料佔整個資料表的百分比成比例減少,而無論聚合索引使用了多少個;
? 日期列不會因為有分秒的輸入而減慢查詢速度;
? 由於改變一個表的內容,將會引起索引的變化。頻繁的insert,update,delete語句將導致系統花費較大的代價進行索引更新,引起整體效能的下降。一般來講,在對查詢效能的要求高於對資料維護效能要求時,應該儘量使用索引,否則,就要慎重考慮一下付出的代價。在某些極端情況下,可先刪除索引,再對資料庫表更新大量資料,最後再重建索引,新建立的索引總是比較好用。

二、 編寫優化的SQL語句,充分利用索引

下面就某些SQL語句的where子句編寫中需要注意的問題作詳細介紹。在這些where子句中,即使某些列存在索引,但是由於編寫了劣質的SQL,系統在執行該SQL語句時也不能使用該索引,而同樣使用全表掃描,這就造成了響應速度的極大降低。
SQL語句在提交給資料庫進行操作前,都會經過查詢分析階段,SQLSERVER內建的查詢優化器會分析查詢條件的的每個部分,並判斷這些條件是否符合掃描引數(SARG)的標準。只有當一個查詢條件符合SARG的標準,才可以通過預先設定的索引,提升查詢效能。
SARG的定義:用於限制搜尋操作的一種規範,通常是指一個特定的匹配,一個確定範圍內的匹配或者兩個以上條件的AND連線。一般形式如下:
列名 操作符 <常數 或 變數>

<常數 或 變數> 操作符 列名
列名可以出現在操作符的一邊,而常數或變量出現在操作符的另一邊。如:
Name=’張三’
價格>5000
5000<價格
Name=’張三’ and 價格>5000
如果一個表示式不能滿足SARG的形式,那它就無法限制搜尋的範圍了,也就是說SQL SERVER必須對每一行都判斷它是否滿足WHERE子句中的所有條件,既進行全表掃描。所以,一個索引對於不滿足SARG形式的表示式來說是無用的, 如:當查詢條件為“價格*2 >5000”時,就無法利用建立在價格欄位上的索引 。
SQLSERVER內建了查詢優化器,能將一些條件自動轉換為符合SARG標準,如:將“價格*2 >5000” 轉換為“價格 >2500/2 ”,以達到可以使用索引的目的,但這種轉化不是100%可靠的,有時會有語義上的損失,有時轉化不了。如果對“查詢優化器”的工作原理不是特別瞭解,寫出的SQL語句可能不會按照您的本意進行查詢。所以不能完全依賴查詢優化器的優化,建議大家還是利用自己的優化知識,儘可能顯式的書寫出符合SARG標準的 SQL語句,自行確定查詢條件的構建方式,這樣一方面有利於查詢分析器分析最佳索引匹配順序,另一方面也有利於今後重讀程式碼。
介紹完SARG後,我們再結合一些實際運用中的例子來做進一步的講解:

1、 Like語句是否屬於SARG取決於使用%萬用字元的樣式

如:name like ‘張%’ ,這就屬於SARG
而:name like ‘%張’ ,就不屬於SARG
萬用字元%在字串首字元的使用會導致索引無法使用,雖然實際應用中很難避免這樣用,但還是應該對這種現象有所瞭解,至少知道此種用法效能是很低下的。

2、 “非”操作符不滿足SARG形式,使得索引無法使用

不滿足SARG形式的語句最典型的情況就是包括非操作符的語句,如:NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等。
下面是一個NOT子句的例子:
... where not (status ='valid')
not運算子也隱式的包含在另外一些邏輯運算子中,比如<>運算子。見下例:
... where status <>'invalid';

再看下面這個例子:
select * from employee where salary<>3000;
對這個查詢,可以改寫為不使用not:
select * from employee where salary<3000 or salary>3000;

雖然這兩種查詢的結果一樣,但是第二種查詢方案會比第一種查詢方案更快些。第二種查詢允許對salary列使用索引,而第一種查詢則不能使用索引。

3、 函式運算不滿足SARG形式,使得索引無法使用

例:下列SQL條件語句中的列都建有恰當的索引,但執行速度卻非常慢:
select * from record where substring(card_no,1,4)=′5378′(13秒)
select * from record where amount/30< 1000(11秒)
select * from record where convert(char(10),date,112)=′19991201′(10秒)
分析:
where子句中對列的任何操作結果都是在SQL執行時逐列計算得到的,因此它不得不進行全表掃描,而沒有使用該列上面的索引;如果這些結果在查詢編譯時就能得到,那麼就可以被SQL優化器優化,使用索引,避免表搜尋,因此將SQL重寫成下面這樣:
select * from record where card_no like ′5378%′(< 1秒)
select * from record where amount < 1000*30(< 1秒)
select * from record where date= ′1999/12/01′ (< 1秒)

你會發現SQL明顯快很多

4、 儘量不要對建立了索引的欄位,作任何的直接處理

select * from employs where first_name + last_name ='beill cliton';
無法使用索引

改為:
select * from employee where
first_name = substr('beill cliton',1,instr('beill cliton',' ')-1)
and
last_name = substr('beill cliton',instr('beill cliton',' ')+1)
則可以使用索引

5、 不同型別的索引效能是不一樣的,應儘可能先使用效能高的

比如:數字型別的索引查詢效率高於字串型別,定長字串char,nchar的索引效率高於變長字串varchar,nvarchar的索引。
應該將
where username='張三' and age>20
改進為
where age>20 and username='張三'
注意:

此處,SQL的查詢分析優化功能可以做到自動重排條件順序,但還是建議預先手工排列好。

6、 儘量不要使用 is null 與 is not null作為查詢條件

  任何包含null值的列都將不會被包含在索引中,如果某列資料中存在空值,那麼對該列建立索引的效能提升是值得懷疑的,尤其是將null作為查詢條件的一部分時。建議一方面避免使用is null和is not null, 另一方面不要讓資料庫欄位中存在null, 即使沒有內容,也應利用預設值,或者手動的填入一個值,如:’’ 空字串。

7、 某些情況下IN 的作用與OR 相當 ,且都不能充分利用索引

例:表stuff有200000行,id_no上有非群集索引,請看下面這個SQL:
select count(*) from stuff where id_no in(′0′,′1′) (23秒)

where條件中的′in′在邏輯上相當於′or′,所以語法分析器會將in (′0′,′1′)轉化為id_no =′0′ or id_no=′1′來執行。我們期望它會根據每個or子句分別查詢,再將結果相加,這樣可以利用id_no上的索引;但實際上,它卻採用了"OR策略",即先取出滿足每個or子句的行,存入臨時資料庫的工作表中,再建立唯一索引以去掉重複行,最後從這個臨時表中計算結果。因此,實際過程沒有利用id_no 上索引,並且完成時間還要受tempdb資料庫效能的影響。
實踐證明,表的行數越多,工作表的效能就越差,當stuff有620000行時,執行時間會非常長!如果確定不同的條件不會產生大量重複值,還不如將or子句分開:
select count(*) from stuff where id_no=′0′
select count(*) from stuff where id_no=′1′
得到兩個結果,再用union作一次加法合算。因為每句都使用了索引,執行時間會比較短,
select count(*) from stuff where id_no=′0′
union
select count(*) from stuff where id_no=′1′
從實踐效果來看,使用union在通常情況下比用or的效率要高的多,而exist關鍵字和in關鍵字在用法上類似,效能上也類似,都會產生全表掃描,效率比較低下,根據未經驗證的說法,exist可能比in要快些。

8、 使用變通的方法提高查詢效率

  like關鍵字支援萬用字元匹配,但這種匹配特別耗時。例如:select * from customer where zipcode like “21_ _ _”,即使在zipcode欄位上已建立了索引,在這種情況下也可能還是採用全表掃描方式。如果把語句改為:select * from customer where zipcode >“21000”,在執行查詢時就會利用索引,大大提高速度。但這種變通是有限制的,不應引起業務意義上的損失,對於郵政編碼而言,zipcode like “21_ _ _” 和 zipcode >“21000” 意義是完全一致的。

9、 組合索引的高效使用

假設已在date,place,amount三個欄位上建立了組合索引
select count(*) from record
where date > ′19991201′ and date < ′19991214′ and amount > 2000
(< 1秒)

select date,sum(amount) from record group by date
(11秒)

select count(*) from record
where date > ′19990901′ and place in (′BJ′,′SH′)
(< 1秒)
這是一個設定較合理的組合索引。它將date作為前導列,使每個SQL都可以利用索引,並且在第一和第三個SQL中形成了索引覆蓋,因而效能達到了最優。如果索引不便於更改,修正SQL中的條件順序以配合索引順序也是可行的。

10、 order by按聚集索引列排序效率最高

排序是較耗時的操作,應儘量簡化或避免對大型表進行排序,如縮小排序的列的範圍,只在有索引的列上排序等等。
我們來看:(gid是主鍵,fariqi是聚合索引列)
select top 10000 gid,fariqi,reader,title from tgongwen
用時:196 毫秒。 掃描計數 1,邏輯讀 289 次,物理讀 1 次,預讀 1527 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc
用時:4720毫秒。 掃描計數 1,邏輯讀 41956 次,物理讀 0 次,預讀 1287 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc
用時:4736毫秒。 掃描計數 1,邏輯讀 55350 次,物理讀 10 次,預讀 775 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc
用時:173毫秒。 掃描計數 1,邏輯讀 290 次,物理讀 0 次,預讀 0 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc
用時:156毫秒。 掃描計數 1,邏輯讀 289 次,物理讀 0 次,預讀 0 次。
從以上我們可以看出,不排序的速度以及邏輯讀次數都是和“order by 聚集索引列” 的速度是相當的,但這些都比“order by 非聚集索引列”的查詢速度是快得多的。
同時,按照某個欄位進行排序的時候,無論是正序還是倒序,速度是基本相當的。

三、 關於節省資料查詢系統開銷方面的措施

1、 使用TOP儘量減少取出的資料量

TOP是SQL SERVER中用來提取前幾條或前某個百分比資料的關鍵詞。
select top 20 gid,fariqi,reader,title from tgongwen order by gid desc
select top 60 percent gid,fariqi,reader,title from tgongwen order by gid desc
在實際的應用中,應該經常利用top 剔除掉不必要的資料,只保留必須的資料集合。這樣不僅可以減少資料庫邏輯讀的次數,還能避免不必要的記憶體浪費,對系統性能的提升都是有好處的。

2、 欄位提取要按照“需多少、提多少”的原則,避免“select *”

這個舉個例子:
select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc
用時:4673毫秒
select top 10000 gid,fariqi,title from tgongwen order by gid desc
用時:1376毫秒
select top 10000 gid,fariqi from tgongwen order by gid desc
用時:80毫秒
由此看來,欄位大小越大,數目越多,select所耗費的資源就越多,比如取int型別的欄位就會比取char的快很多。我們每少提取一個欄位,資料的提取速度就會有相應的提升。提升的幅度根據捨棄的欄位的大小來判斷。

3、 count(*) 與 count(欄位) 方法比較

我們來看一些實驗例子(gid為Tgongwen的主鍵):
select count(*) from Tgongwen
用時:1500毫秒
select count(gid) from Tgongwen
用時:1483毫秒
select count(fariqi) from Tgongwen
用時:3140毫秒
select count(title) from Tgongwen
用時:52050毫秒
從以上可以看出,用count(*)和用count(主鍵)的速度是相當的,而count(*)卻比其他任何除主鍵以外的欄位彙總速度要快,而且欄位越長,彙總速度就越慢。如果用count(*), SQL SERVER會自動查詢最小欄位來彙總。當然,如果您直接寫count(主鍵)將會來的更直接些。

4、 有巢狀查詢時,儘可能在內層過濾掉資料

如果一個列同時在主查詢和where子句中出現,很可能當主查詢中的列值改變之後,子查詢必須重新查詢一次。而且查詢巢狀層次越多,效率越低,因此應當儘量避免子查詢。如果子查詢不可避免,那麼要在子查詢中過濾掉儘可能多的行。

5、 多表關聯查詢時,需注意表順序,並儘可能早的過濾掉資料

在使用Join進行多表關聯查詢時候,應該使用系統開銷最小的方案。連線條件要充份考慮帶有索引的表、行數多的表,並注意優化表順序;說的簡單一點,就是儘可能早的將之後要做關聯的資料量降下來。
一般情況下,sqlserver 會對錶的連線作出自動優化。例如:
select name,no from A
join B on A. id=B.id
join C on C.id=A.id
where name='wang'
儘管A表在From中先列出,然後才是B,最後才是C。但sql server可能會首先使用c表。它的選擇原則是相對於該查詢限制為單行或少數幾行,就可以減少在其他表中查詢的總資料量。絕大多數情況下,sql server 會作出最優的選擇,但如果你發覺某個複雜的聯結查詢速度比預計的要慢,就可以使用SET FORCEPLAN語句強制sql server按照表出現順序使用表。如上例加上:SET FORCEPLAN ON…….SET FORCEPLAN OFF 表的執行順序將會按照你所寫的順序執行。在查詢分析器中檢視2種執行效率,從而選擇表的連線順序。SET FORCEPLAN的缺點是隻能在儲存過程中使用。

小結:

? 聚集索引比較寶貴,應該用在查詢頻率最高的地方;
? 在資料為“既不是絕大多數相同,也不是極少數相同”狀態時,
最能發揮聚集索引的潛力;
? 複合索引的設定和使用要注意保持順序一致;
? 條件子句的表示式最好符合SARG規範,是可利用索引的;
? 任何對列的操作都導致全表掃描,如資料庫函式、計算表示式等,
查詢時應儘可能將操作移至等號的某一邊;
? 要注意含有null值時,是不能充分利用索引的;
? exist, in、or等子句常會使索引失效;
如果不產生大量重複值,可以考慮把子句拆開,再用union拼合;
? 排序時應充分利用帶索引的欄位;
? 儘可能早,快的過濾掉無用的資料,只將必須的資料帶到後續的操作中去
從前面講敘的內容可以看出,SQL語句優化的實質就是在結果正確的前提下,用分析優化器可以識別的SARG規範語句,充份利用索引,減少資料的I/O次數,儘量避免全表掃描的發生。
以上內容有些是指導性的理論原則,有些是實際摸索的經驗,大家在使用時應靈活處理,根據實際情況,選擇合適的方法。本文中列舉的實驗資料僅作比對用,不具備普遍意義。大家在實際專案中,應充分利用效能監測和分析工具(如SQLSERVER帶的相關工具)來檢驗自己的優化效果。
此外,還有很重要的一點要提醒大家,同樣複雜的資料操作,在SQLSERVER資料庫級別完成的代價要遠遠小於在應用端用程式程式碼完成的代價,所以建議大家全面,深入的學習SQL語法中重要關鍵字的應用,如:Group By ,Having等,儘量把資料操作任務放在資料庫系統中完成。資料庫應用系統的效能優化是一個複雜的過程,上述這些只是在SQL語句層次的一種體現,深入研究還會涉及資料庫層的資源配置、網路層的流量控制以及作業系統層的總體設計等等,這些將在以後的文章中詳細論述。