1. 程式人生 > >優化SQL查詢:怎樣寫出高效能SQL語句

優化SQL查詢:怎樣寫出高效能SQL語句

1、 首先要搞明白什麼叫執行計劃?

執行計劃是資料庫根據SQL語句和相關表的統計資訊作出的一個查詢方案,這個方案是由查詢優化器自動分析產生的,比如一條SQL語句如果用來從一個 10萬條記錄的表中查1條記錄,那查詢優化器會選擇“索引查詢”方式,如果該表進行了歸檔,當前只剩下5000條記錄了,那查詢優化器就會改變方案,採用 “全表掃描”方式。

可見,執行計劃並不是固定的,它是“個性化的”。產生一個正確的“執行計劃”有兩點很重要:

(1) SQL語句是否清晰地告訴查詢優化器它想幹什麼?

(2) 查詢優化器得到的資料庫統計資訊是否是最新的、正確的?

2、 統一SQL語句的寫法
對於以下兩句SQL語句,程式設計師認為是相同的,資料庫查詢優化器認為是不同的。

select*from dual

select*From dual
其實就是大小寫不同,查詢分析器就認為是兩句不同的SQL語句,必須進行兩次解析。生成2個執行計劃。所以作為程式設計師,應該保證相同的查詢語句在任何地方都一致,多一個空格都不行!

3、 不要把SQL語句寫得太複雜

我經常看到,從資料庫中捕捉到的一條SQL語句打印出來有2張A4紙這麼長。一般來說這麼複雜的語句通常都是有問題的。我拿著這2頁長的SQL語句去請教原作者,結果他說時間太長,他一時也看不懂了。可想而知,連原作者都有可能看糊塗的SQL語句,資料庫也一樣會看糊塗。

一般,將一個Select語句的結果作為子集,然後從該子集中再進行查詢,這種一層巢狀語句還是比較常見的,但是根據經驗,超過3層巢狀,查詢優化 器就很容易給出錯誤的執行計劃。因為它被繞暈了。像這種類似人工智慧的東西,終究比人的分辨力要差些,如果人都看暈了,我可以保證資料庫也會暈的。

另外,執行計劃是可以被重用的,越簡單的SQL語句被重用的可能性越高。而複雜的SQL語句只要有一個字元發生變化就必須重新解析,然後再把這一大堆垃圾塞在記憶體裡。可想而知,資料庫的效率會何等低下。

4、 使用“臨時表”暫存中間結果

簡化SQL語句的重要方法就是採用臨時表暫存中間結果,但是,臨時表的好處遠遠不止這些,將臨時結果暫存在臨時表,後面的查詢就在tempdb中了,這可以避免程式中多次掃描主表,也大大減少了程式執行中“共享鎖”阻塞“更新鎖”,減少了阻塞,提高了併發效能。

5、 OLTP系統SQL語句必須採用繫結變數

selectfrom orderheader where changetime >‘2010-10-20 00:00:01’
select

from orderheader where changetime >‘2010-09-22 00:00:01’
以上兩句語句,查詢優化器認為是不同的SQL語句,需要解析兩次。如果採用繫結變數
select*from orderheader where changetime >@chgtime

@chgtime變數可以傳入任何值,這樣大量的類似查詢可以重用該執行計劃了,這可以大大降低資料庫解析SQL語句的負擔。一次解析,多次重用,是提高資料庫效率的原則。

6、 繫結變數窺測

事物都存在兩面性,繫結變數對大多數OLTP處理是適用的,但是也有例外。比如在where條件中的欄位是“傾斜欄位”的時候。

“傾斜欄位”指該列中的絕大多數的值都是相同的,比如一張人口調查表,其中“民族”這列,90%以上都是漢族。那麼如果一個SQL語句要查詢30歲的漢族人口有多少,那“民族”這列必然要被放在where條件中。這個時候如果採用繫結變數@nation會存在很大問題。

試想如果@nation傳入的第一個值是“漢族”,那整個執行計劃必然會選擇表掃描。然後,第二個值傳入的是“布依族”,按理說“布依族”佔的比例 可能只有萬分之一,應該採用索引查詢。但是,由於重用了第一次解析的“漢族”的那個執行計劃,那麼第二次也將採用表掃描方式。這個問題就是著名的“繫結變 量窺測”,建議對於“傾斜欄位”不要採用繫結變數。

7、 只在必要的情況下才使用begin tran

SQL Server中一句SQL語句預設就是一個事務,在該語句執行完成後也是預設commit的。其實,這就是begin tran的一個最小化的形式,好比在每句語句開頭隱含了一個begin tran,結束時隱含了一個commit。

有些情況下,我們需要顯式宣告begin tran,比如做“插、刪、改”操作需要同時修改幾個表,要求要麼幾個表都修改成功,要麼都不成功。begin tran 可以起到這樣的作用,它可以把若干SQL語句套在一起執行,最後再一起commit。好處是保證了資料的一致性,但任何事情都不是完美無缺的。Begin tran付出的代價是在提交之前,所有SQL語句鎖住的資源都不能釋放,直到commit掉。

可見,如果Begin tran套住的SQL語句太多,那資料庫的效能就糟糕了。在該大事務提交之前,必然會阻塞別的語句,造成block很多。

Begin tran使用的原則是,在保證資料一致性的前提下,begin tran 套住的SQL語句越少越好!有些情況下可以採用觸發器同步資料,不一定要用begin tran。

8、 一些SQL查詢語句應加上nolock

在SQL語句中加nolock是提高SQL Server併發效能的重要手段,在oracle中並不需要這樣做,因為oracle的結構更為合理,有undo表空間儲存“資料前影”,該資料如果在修 改中還未commit,那麼你讀到的是它修改之前的副本,該副本放在undo表空間中。這樣,oracle的讀、寫可以做到互不影響,這也是oracle 廣受稱讚的地方。SQL Server 的讀、寫是會相互阻塞的,為了提高併發效能,對於一些查詢,可以加上nolock,這樣讀的時候可以允許寫,但缺點是可能讀到未提交的髒資料。使用 nolock有3條原則。

(1) 查詢的結果用於“插、刪、改”的不能加nolock !

(2) 查詢的表屬於頻繁發生頁分裂的,慎用nolock !

(3) 使用臨時表一樣可以儲存“資料前影”,起到類似oracle的undo表空間的功能,

能採用臨時表提高併發效能的,不要用nolock 。

9、 聚集索引沒有建在表的順序欄位上,該表容易發生頁分裂

比如訂單表,有訂單編號orderid,也有客戶編號contactid,那麼聚集索引應該加在哪個欄位上呢?對於該表,訂單編號是順序新增的,如 果在orderid上加聚集索引,新增的行都是新增在末尾,這樣不容易經常產生頁分裂。然而,由於大多數查詢都是根據客戶編號來查的,因此,將聚集索引加 在contactid上才有意義。而contactid對於訂單表而言,並非順序欄位。

比如“張三”的“contactid”是001,那麼“張三”的訂單資訊必須都放在這張表的第一個資料頁上,如果今天“張三”新下了一個訂單,那該訂單資訊不能放在表的最後一頁,而是第一頁!如果第一頁放滿了呢?很抱歉,該表所有資料都要往後移動為這條記錄騰地方。

SQL Server的索引和Oracle的索引是不同的,SQL Server的聚集索引實際上是對錶按照聚集索引欄位的順序進行了排序,相當於oracle的索引組織表。SQL Server的聚集索引就是表本身的一種組織形式,所以它的效率是非常高的。也正因為此,插入一條記錄,它的位置不是隨便放的,而是要按照順序放在該放的 資料頁,如果那個資料頁沒有空間了,就引起了頁分裂。所以很顯然,聚集索引沒有建在表的順序欄位上,該表容易發生頁分裂。

曾經碰到過一個情況,一位哥們的某張表重建索引後,插入的效率大幅下降了。估計情況大概是這樣的。該表的聚集索引可能沒有建在表的順序欄位上,該表 經常被歸檔,所以該表的資料是以一種稀疏狀態存在的。比如張三下過20張訂單,而最近3個月的訂單隻有5張,歸檔策略是保留3個月資料,那麼張三過去的 15張訂單已經被歸檔,留下15個空位,可以在insert發生時重新被利用。在這種情況下由於有空位可以利用,就不會發生頁分裂。但是查詢效能會比較 低,因為查詢時必須掃描那些沒有資料的空位。

重建聚集索引後情況改變了,因為重建聚集索引就是把表中的資料重新排列一遍,原來的空位沒有了,而頁的填充率又很高,插入資料經常要發生頁分裂,所以效能大幅下降。

對於聚集索引沒有建在順序欄位上的表,是否要給與比較低的頁填充率?是否要避免重建聚集索引?是一個值得考慮的問題!

10、加nolock後查詢經常發生頁分裂的表,容易產生跳讀或重複讀

加nolock後可以在“插、刪、改”的同時進行查詢,但是由於同時發生“插、刪、改”,在某些情況下,一旦該資料頁滿了,那麼頁分裂不可避免,而 此時nolock的查詢正在發生,比如在第100頁已經讀過的記錄,可能會因為頁分裂而分到第101頁,這有可能使得nolock查詢在讀101頁時重複 讀到該條資料,產生“重複讀”。同理,如果在100頁上的資料還沒被讀到就分到99頁去了,那nolock查詢有可能會漏過該記錄,產生“跳讀”。

上面提到的哥們,在加了nolock後一些操作出現報錯,估計有可能因為nolock查詢產生了重複讀,2條相同的記錄去插入別的表,當然會發生主鍵衝突。

11、使用like進行模糊查詢時應注意

有的時候會需要進行一些模糊查詢比如
select*from contact where username like ‘%yue%’

關鍵詞%yue%,由於yue前面用到了“%”,因此該查詢必然走全表掃描,除非必要,否則不要在關鍵詞前加%,

12、資料型別的隱式轉換對查詢效率的影響

sql server2000的資料庫,我們的程式在提交sql語句的時候,沒有使用強型別提交這個欄位的值,由sql server 2000自動轉換資料型別,會導致傳入的引數與主鍵欄位型別不一致,這個時候sql server 2000可能就會使用全表掃描。Sql2005上沒有發現這種問題,但是還是應該注意一下。

13、SQL Server 表連線的三種方式

(1) Merge Join

(2) Nested Loop Join

(3) Hash Join

SQL Server 2000只有一種join方式——Nested Loop Join,如果A結果集較小,那就預設作為外表,A中每條記錄都要去B中掃描一遍,實際掃過的行數相當於A結果集行數x B結果集行數。所以如果兩個結果集都很大,那Join的結果很糟糕。

SQL Server 2005新增了Merge Join,如果A表和B表的連線欄位正好是聚集索引所在欄位,那麼表的順序已經排好,只要兩邊拼上去就行了,這種join的開銷相當於A表的結果集行數加 上B表的結果集行數,一個是加,一個是乘,可見merge join 的效果要比Nested Loop Join好多了。

如果連線的欄位上沒有索引,那SQL2000的效率是相當低的,而SQL2005提供了Hash join,相當於臨時給A,B表的結果集加上索引,因此SQL2005的效率比SQL2000有很大提高,我認為,這是一個重要的原因。

總結一下,在表連線時要注意以下幾點:

(1) 連線欄位儘量選擇聚集索引所在的欄位

(2) 仔細考慮where條件,儘量減小A、B表的結果集

(3) 如果很多join的連線欄位都缺少索引,而你還在用SQL Server 2000,趕緊升級吧。

如何為資料庫新增索引:
為方便講述,我們先建立一個如下的表。

CREATE TABLE mytable (
     id serial primary key,
     category_id int not null default 0,
     user_id int not null default 0,
     adddate int not null default 0
  );

很簡單吧,不過對於要說明這個問題,已經足夠了。如果你在查詢時常用類似以下的語句:

SELECT * FROM mytable WHERE category_id=1;

最直接的應對之道,是為category_id建立一個簡單的索引:

CREATE INDEX mytable_categoryid
     ON mytable (category_id);

OK,搞定?先別高興,如果你有不止一個選擇條件呢?例如:

SELECT * FROM mytable WHERE category_id=1 AND user_id=2;

你的第一反應可能是,再給user_id建立一個索引。不好,這不是一個最佳的方法。你可以建立多重的索引。

CREATE INDEX mytable_categoryid_userid ON mytable (category_id,user_id);

注意到我在命名時的習慣了嗎?我使用"表名_欄位1名_欄位2名"的方式。你很快就會知道我為什麼這樣做了。

現在你已經為適當的欄位建立了索引,不過,還是有點不放心吧,你可能會問,資料庫會真正用到這些索引嗎?測試一下就OK,對於大多數的資料庫來說,這是很容易的,只要使用EXPLAIN命令:

EXPLAIN

SELECT * FROM mytable
    WHERE category_id=1 AND user_id=2;

This is what Postgres 7.1 returns (exactly as I expected)

NOTICE: QUERY PLAN:

Index Scan using mytable_categoryid_userid on
     mytable (cost=0.00…2.02 rows=1 width=16)

EXPLAIN

以上是postgres的資料,可以看到該資料庫在查詢的時候使用了一個索引(一個好開始),而且它使用的是我建立的第二個索引。看到我上面命名的好處了吧,你馬上知道它使用適當的索引了。

接著,來個稍微複雜一點的,如果有個ORDER BY字句呢?不管你信不信,大多數的資料庫在使用order by的時候,都將會從索引中受益。

SELECT * FROM mytable
    WHERE category_id=1 AND user_id=2
     ORDER BY adddate DESC;

有點迷惑了吧?很簡單,就象為where字句中的欄位建立一個索引一樣,也為ORDER BY的字句中的欄位建立一個索引:

CREATE INDEX mytable_categoryid_userid_adddate
     ON mytable (category_id,user_id,adddate);

注意: “mytable_categoryid_userid_adddate” 將會被截短為

“mytable_categoryid_userid_addda”

CREATE

EXPLAIN SELECT * FROM mytable
    WHERE category_id=1 AND user_id=2
     ORDER BY adddate DESC;

NOTICE: QUERY PLAN:

Sort (cost=2.03…2.03 rows=1 width=16)
    -> Index Scan using mytable_categoryid_userid_addda
       on mytable (cost=0.00…2.02 rows=1 width=16)

EXPLAIN

看看EXPLAIN的輸出,好象有點恐怖啊,資料庫多做了一個我們沒有要求的排序,這下知道效能如何受損了吧,看來我們對於資料庫的自身運作是有點過於樂觀了,那麼,給資料庫多一點提示吧。

為了跳過排序這一步,我們並不需要其它另外的索引,只要將查詢語句稍微改一下。這裡用的是postgres,我們將給該資料庫一個額外的提示–在ORDER BY語句中,加入where語句中的欄位。這只是一個技術上的處理,並不是必須的,因為實際上在另外兩個欄位上,並不會有任何的排序操作,不過如果加入,postgres將會知道哪些是它應該做的。

EXPLAIN SELECT * FROM mytable
    WHERE category_id=1 AND user_id=2
     ORDER BY category_id DESC,user_id DESC,adddate DESC;

NOTICE: QUERY PLAN:

Index Scan Backward using
    mytable_categoryid_userid_addda on mytable
     (cost=0.00…2.02 rows=1 width=16)

EXPLAIN

現在使用我們料想的索引了,而且它還挺聰明,知道可以從索引後面開始讀,從而避免了任何的排序。

以上說得細了一點,不過如果你的資料庫非常巨大,並且每日的頁面請求達上百萬算,我想你會獲益良多的。不過,如果你要做更為複雜的查詢呢,例如將多張表結合起來查詢,特別是where限制字句中的欄位是來自不止一個表格時,應該怎樣處理呢?我通常都儘量避免這種做法,因為這樣資料庫要將各個表中的東西都結合起來,然後再排除那些不合適的行,搞不好開銷會很大。

如果不能避免,你應該檢視每張要結合起來的表,並且使用以上的策略來建立索引,然後再用EXPLAIN命令驗證一下是否使用了你料想中的索引。如果是的話,就OK。不是的話,你可能要建立臨時的表來將他們結合在一起,並且使用適當的索引。

要注意的是,建立太多的索引將會影響更新和插入的速度,因為它需要同樣更新每個索引檔案。對於一個經常需要更新和插入的表格,就沒有必要為一個很少使用的where字句單獨建立索引了,對於比較小的表,排序的開銷不會很大,也沒有必要建立另外的索引。

以上介紹的只是一些十分基本的東西,其實裡面的學問也不少,單憑EXPLAIN我們是不能判定該方法是否就是最優化的,每個資料庫都有自己的一些優化器,雖然可能還不太完善,但是它們都會在查詢時對比過哪種方式較快,在某些情況下,建立索引的話也未必會快,例如索引放在一個不連續的儲存空間時,這會增加讀磁碟的負擔,因此,哪個是最優,應該通過實際的使用環境來檢驗。

在剛開始的時候,如果表不大,沒有必要作索引,我的意見是在需要的時候才作索引,也可用一些命令來優化表,例如MySQL可用"OPTIMIZE TABLE"。