sql優化與鎖

 

【DB2】Sql優化與鎖

分類: 技術 257人閱讀 評論(0) 收藏 舉報

本次XX項目性能測試,80%性能的提升在於Sql和索引的修改。總結有以下幾點:

1) 不高效的sql(不合理的sql)

2) 不合理的索引(如何建立合理的索引)

3) 避免死鎖和大量鎖等待

下面針對這3個方面總結下要點。

1.編寫高效的Sql注意要點

1.1 表連接

表連接有兩個要點:

1) 表連接順序

2) 連接條件

Sql_stmt_1:

Select * from A left join B on A.id=B.id join C on B.id = C.C_id where A.con=’ ’ and B.con=’ ’

一般情況下,DB2會根據各表的JOIN順序自頂向下處理,即從Sql來看,就是自左向右解析,先A、B做連接操作,之後會產生結果集,將會寫入內存,如果內存不夠,會寫入臨時表空間,之後會用結果集和C做連接操作。如果sql中只有兩表連接,那麼其前後順序沒什麼關係,優化器會自己去評估。而如果sql中存在超過2個表連接時,那麼表連接就會有順序之分。那麼,原則是:

如果sql中存在表A、B、C三表連接,則首先應保證最先連接的兩表具有較小的子集。

在進行表連接時,需要提供連接字段(即On語法後的等價謂詞,on A.id=B.id)。此時,我們需要保證,連接字段存在索引。這樣當結果集小時,會走NestJoin(速度快,因爲會利用到索引),當結果集大時,會走Hash join。此外,在對A、B表進行連接時,優化器需要判斷採用何種連接類型,這時會先執行where 字句後的條件。也就是說,如果where字句能過濾很多的條件,那麼表連接的結果集就會很小,cost自然會降低,所以適當爲where字句的查詢字段建立索引,能夠得到更好的性能。原則:

在進行表連接時,爲連接字段和查詢過濾字段(where 字句後的條件)建立索引,會得到很好的性能提升。

在本次測試中,發現有的sql會在表連接時,爲其指定多個連接條件,形如:

SELECT B.APPROVE_STATUS, count ( * ) AS NUM

  FROM    BIZ.WF_TASK C

       LEFT JOIN

          BIZ.REI_FORM B

       ON C.RECEIPT_NO = B.REI_FORM_ID

 WHERE C.TASK_STATUS = '01'

   AND C.HANDLE_ID = '1234560000102'

   AND (C.RECEIPT_TYPE = '02' OR C.RECEIPT_TYPE = '03')

GROUP BY B.APPROVE_STATUS

  WITH UR

執行cost:


如果sql寫成(增加一個表連接字段):

SELECT B.APPROVE_STATUS, count ( * ) AS NUM

  FROM    BIZ.WF_TASK C

       LEFT JOIN

          BIZ.REI_FORM B

       ON C.RECEIPT_NO = B.REI_FORM_ID

      AND (C.RECEIPT_TYPE = '02' OR C.RECEIPT_TYPE = '03')

 WHERE C.TASK_STATUS = '01' AND C.HANDLE_ID = '1234560000102'

GROUP BY B.APPROVE_STATUS

  WITH UR


對比結果,我們可以看到,當連接條件存在多個時,cost會高很多,因爲多做了一次表連接。如果是小表,看不出差別,如果是大表關聯,則結果很明顯。原則:

當進行表連接時,請確保連接條件只有一個,尤其是大表連接。

1.2 合理使用Not in 和Not Exists

雖然Not in 和Not exits 可以實現相同的功能,但是兩者本身的實現方式不同:

Not In:是自內向外操作,即先得到子查詢結果,然後執行外層查詢。包含not in 子句的執行順序是:首先取外部一個查詢結果與內部子集比較,不管是否存在,它都要遍歷整個子集,往往無法利用到索引,因而是由內向外過程。所以,當內部查詢子集很大時,就會具有較高的查詢代價。

 Not Exists:恰恰相反,是外向內操作。即先執行外部查詢結果,然後再執行內部操作,

是集合操作。包含 not exists子句的執行順序是:首先取外部一個查詢結果與內部子集比較,若存在即刻返回,而不需要便利整個子集,如果存在索引,就會使用索引,因而是個自外而內的過程。所以,當內部子集很大時,相對來說,性能要優於Not in

因而,總的來說,Not exits在整體性能上要由於Not in。原則:

當子查詢結果集較大時,Not exists 較 Not in 具有較高的性能提升;

當子查詢結果集較小時(個數或者百數以內),兩者相差不多,一般來說,此時Not in 會教優於Not exists。就好像表數據小時,全表掃描總是要由於索引掃描;

當子查詢具有一定的複雜度時(即sql關聯關係較多,如子查詢句中包含多個表查詢),由於內部查詢的複雜度,會導致Not exists 查詢具有較大的複雜度,降低性能。此時可以考慮採用Not in。

INExists兩者相差不多,這裏不做比較,思路形同。

1.3 改寫 OR和不等於(!=||<>

我們在編寫sql時,通常都會按照程序邏輯去寫,此時,當我們遇到如下場景:

我要查詢企業員工表(employee)中的員工狀態爲實習(type=’01’)或者兼職的所有員工(type=’08’),假設狀態共有10

此時,我們立馬會寫如下Sql

Select * from employee A  where A.type=’01’ or A.type=’08’ 

我們假設,在type列上存在索引。而此Sql含有or運算,對於優化器來說,因爲無法運用到一個範圍,所以無法利用索引掃描。而通常此種情況需要遍歷所有記錄或者所有索引。這樣會明顯提高查詢cost。我們希望是通過索引的方式,畢竟該表是個大表,如果出現大表掃描,多系統性能有很大的影響。那麼可以採取用UNION改寫OR子句,如下:

Select * from employee A  where A.type=’01’  union 

Select * from employee A  where A.type=’02  

改寫成上述sql,優化器會分別執行兩個查詢子集,然後union合併。這樣就可以利用到索引(type=01’)。當然Union包含去除重複元素的功能,即相當於distinct,這樣就會有排序存在,如果業務場景允許,可以考慮使用union all,它和union不同的是,它無需排序去重,只需要兩個子集合並即刻。效率要高於union。原則是:

當存在大表鏈接且連接條件較多,並且連接條件包含Or子句時,建議使用Union/Union all來替換。

對於不等與來說也是類似,不等於在邏輯上其實是類似於 Not 的概念。

如,對如下sql:

Sql_stmt_2:

Select * from employee where type !=’01’

所以我們可以有如下改寫方式:

1) 將<>改寫爲Not in操作,即

Select * from employee where type not in (‘01’)

2) 將<>改寫爲大於和小於的結合

Select * from employee where type >’01’ union 

Select * from employee where type <’01’(當然如果你知道一個大於已經足夠,那麼完全可以省略掉小於的操作,這就是分析sql的業務場景)

顯然,對於1)的改法,它適用與Not in 子集中有多個值的情況;對於2)改法,要要由於1),因爲它可以利用到Type列上的索引。

原則是:

當存在大表鏈接且連接條件較多,並且連接條件包含不等於(<>||!=)子句時,建議使用Union/Union all 聯合大於小於操作來替換。

1.4 利用子查詢結果

將查詢結果作爲子查詢,主要是爲了減少掃描的數據量,以及利用索引進行數據檢索。尤其是針對大表來說。它的特點就是,在進行查詢之前,先用子查詢將結果集過濾到最小,並且通常這時候的過濾謂詞是存在索引的。

假設如下情況:

索引:

Inst1.idx_history_date on inst1.history(tstmp)

Inst1.idx_history_acct on inst1.history(acct_id)

查詢:

Select a.name from inst1.acct a ,inst1.history h where a.acct_id = h.acct_id and (h.tstmp > current timestatmp – 2 days or a.balance>100);

上面的查詢用於選擇餘額大於100元或者最近兩天有過交易的賬戶名稱。由於不存在組合索引(acct_id,tstmp),它們是單獨字段建立索引,所以對於上述的查詢無法利用索引,將會很不幸的走全表掃描。那麼改寫成如下方式,可以奏效:

With tmp as (select acct_id from inst1.history h where h.tstmp > current timestatmp -2 days) select name from inst1.acct a where a.acct_id in (select acct_id from tmp ) or a.balance >100;

這樣改寫以後,子查詢結果tmp就會走索引inst1.idx_history_date,並且會過濾掉表history一定的結果。然後再與acct表連接,走索引Inst1.idx_history_acct。原則:

當查詢謂詞條件存在兩個以上,並且該謂詞的選擇性很強,作爲單獨一列,未組合索引,此時可以考慮採用構建子查詢,來利用索引縮減掃描的數據量。

1.5 其他注意小點

Ø 避免select * from。。。的使用,取所需的列即可。當表很小時,看不出來fetch所有列帶來的開銷,但是當表很大時,除了索引列外,*號代表取出所有字段將會有很高的fetch

Ø 避免在索引列上做運算,如substr分割字符串,它將會失去索引的判斷性,很有可能無法利用到索引

Ø 當查詢返回結果集較多時,而我們卻不需要這麼多,那麼可以採用fetch first N rows onlyN爲大於0的整數,這樣即使你做了全表掃描,但是我只會取前100行數據,會大大降低執行時間。

Ø 對於查詢出來的結果集是隻讀的,使用select …for read only(或者fetch only),意味着後續不會對該些數據行做updatedelete操作,這可以幫助DB2提高Fetch性能。因爲它允許DB2執行塊操作,一個fetch操作可以請求返回多行數據

Ø 根據業務邏輯選擇合適粒度的隔離級別。UR對於系統有最大的併發性,但也有更多的數據問題,髒讀、幻想讀都會發生。如果系統可以接受這樣的查詢結果,那麼UR是最好的選擇。DB2默認是CS隔離級別,在大併發下,該隔離級別是有可能導致大量鎖等待和死鎖。所以在編寫sql時,考慮業務場景,針對查詢,爲sql賦予一定的隔離級別。

Ø 避免不必要的排序。排序是數據庫中資源消耗比較大的一種操作。在業務允許下,通過添加索引(索引本身就是有序的,確保索引排序和業務排序相符);在distinctgroup byorder by 子句涉及的列上創建索引(注意索引的排序是升序asc還是降序desc

2.如何建立合理的索引

在項目開發過程中,我們也可以根據需要定義索引,比如當表結構和Sql穩定後,我們便可以根據該sql執行的頻率來決定是否需要爲該sql建立索引。Sql中類似where 子句後就單個謂詞,我們比較容易建立索引,而如果是多表關聯並且謂詞關係較多時,我們可以先採用Db2 提供的索引優化工具Db2Advis來幫助我們建立索引,至少它可以綜合各個表的存量和各個列的佔比爲我們提供建議。語法如下:

db2advis -d dbname -i test1.sql -n schema_name -t 5 > wf_task.adv

注:將上述加粗的參數用自己的數據庫參數代替;

    dbname 是數據庫名稱

    Test1.sql 是存放你待提供建議sql的文件名稱

    schema_name 是你建立索引所在的視圖,這裏和表保持一致就好

或者用如下Sql

db2advis -d dbname -s "SELECT * FROM T1 ORDER BY EMPNO" -m IMCP

注:這裏只需要將dbname換成自己的數據庫名稱,sql statement換成自己的

爲了對比索引建立前後帶來的不同,除了cost之外,我們還可以去查看它的執行計劃,觀察走索引到底比沒有索引快了多少。我們仍然使用DB2提供的工具 dynexpln或者db2expln語法如下:

db2expln –d dbname –s –g –q “sql statement” –t  或者

dynexpln –d dbname –s –g –q “sql statement” -t

此外在建立索引時,需要注意以下幾點:

1) 根據條件中謂詞的選擇度創建索引

可以簡單的通過select count(*) from tabname where col=’X’這樣的方式,觀察每個謂詞條件過濾的總數。過濾出結果集越小,代表選擇度越高,如果是建立組合索引,那麼應該將該謂詞放在首位

2) 避免在建有索引的列上使用函數

3) 在需要被排序的列上建立索引(注意索引的順序與排序順序一致),對大表很有效

4) 使用include關鍵詞創建索引

存在這種情況,當表足夠大時(通常是百萬級),我們需要通過一個謂詞col(是唯一的)來獲取列中的colA,這時候,如果將colA加入索引很浪費,因爲它並沒有很大選擇性,而如果不加入,當表很大時,fetch cost太高。這是可以用include來將列colA包含進索引,這樣不再有多餘並且耗時的fetchinclude列也不會影響索引的選擇性。建立include類索引,必須要求索引字段是唯一的,否則無法include

3.避免死鎖和鎖等待

數據庫中之所以會存在死鎖或者鎖等待,是因爲某一事務執行時間過長,導致鎖沒有及時釋放,那麼我們的解決辦法就是,事務過程儘量要短,並且事務中的sql執行要快,這樣纔不會有過多的鎖等待。還有一個原因,就是一些執行糟糕的sql,比如走了全表掃描,那麼它會佔據表中大量的鎖,導致鎖住了其他行,其他用戶只能等待。

解決鎖等待,要注意以下幾點:

Ø 優化查詢 Sql,採用db2advis建立合適的索引,使得其能夠走索引查詢,由於索引的範圍和排序,可以直接跳過許其他行,定位到符合我們需要的行。

Ø 採用合適的隔離級別。由於DB2 默認是CS的隔離級別,它的原理是,遊標每到一行就會鎖住改行,對於一般應用來說是足夠了,但是如果遇到全表掃描,那麼CS模式會鎖住表中大量的行,直到查詢完畢。所以可以根據業務需求,將其改爲UR模式,它不會對錶加任何行鎖。或者在JDBC中設置隔離級別(Isolation Levels)

Ø 合理設置鎖超時參數,它主要是用來避免事務長時間被佔用,導致鎖和連接無法釋放,影響系統的併發。可以設置 DB參數

Ø 更新操作一定要走索引,否則很容易產生死鎖。(針對邊更新邊查的操作)

Ø 避免出現鎖升級現象,當鎖等待達到一定程度時(行鎖的個數超過loctList *percent of lock list),就會出現行鎖升級爲表鎖,即鎖升級。因爲一旦出現鎖升級,那麼鎖住的就不再是行,而是表,那麼其他事務要想訪問該表中的任意行,必須等待事務將鎖釋放。


修改Lock timeout -1代表不檢測鎖超時),一般來說,該參數默認爲10s足矣。
當系統存在嚴重的鎖等待時,可以通過以下sql,定位到鎖等待Sql

db2  "select AGENT_ID ,substr(STMT_TEXT,1,100) as statement,STMT_ELAPSED_TIME_MS  from table(SNAPSHOT_STATEMENT('dbname',-1)) as B where AGENT_ID in (select AGENT_ID_HOLDING_LK from table(SNAPSHOT_LOCKWAIT(‘dbname’,-1)) as A  order by  LOCK_WAIT_START_TIME ASC FETCH FIRST 20 ROWS ONLY ) order by STMT_ELAPSED_TIME_MS DESC" 

運行結果如下:


死鎖比鎖超時更加可怕,因爲它將隨機回滾一個事務,而這個不受應用程序控制,不可控的錯誤十分可怕,所以一旦出現死鎖,必須解決掉。如何觀察DB2是否存在死鎖呢,有以下兩種方式:

1) 開啓lock快照監控

db2 update monitor switches using LOCK on 

執行如下命令:

db2 get snapshot for database on dbname | grep -i "LOCK" ,結果如下:


可以看到其中有DeadLocks detected

2) 採用db2top 工具(db2 V9.1後纔有)

Db2top –d dbname

然後鍵盤輸入‘d’,如下:


可以看到其中有個 DeadLocks 計數。

如何定位死鎖也有很多方式,如:

1) 創建死鎖監控器(需要針對文件分析,複雜度較高)

2) 採用db2pd(最穩定,一般可以定位到)

3) 採用db2top 監控組件(最快,但不一定能抓到)

附件中,講述方法2),該方法我在差旅測試過程中發現,並且總是可以精確定位到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章