要正確的優化SQL,咱們須要快速定位能性的瓶頸點,也就是說快速找到咱們SQL主要的開銷在哪裏?而大多數狀況性能最慢的設備會是瓶頸點,以下載時網絡速度可能會是瓶頸點,本地複製文件時硬盤可能會是瓶頸點,爲何這些通常的工做咱們能快速確認瓶頸點呢,由於咱們對這些慢速設備的性能數據有一些基本的認識,如網絡帶寬是2Mbps,硬盤是每分鐘7200轉等等。所以,爲了快速找到SQL的性能瓶頸點,咱們也須要了解咱們計算機系統的硬件基本性能指標,下圖展現的當前主流計算機性能指標數據。html
從圖上能夠看到基本上每種設備都有兩個指標:java
延時(響應時間):表示硬件的突發處理能力;mysql
帶寬(吞吐量):表明硬件持續處理能力。程序員
從上圖能夠看出,計算機系統硬件性能從高到代依次爲:面試
CPU——Cache(L1-L2-L3)——內存——SSD硬盤——網絡——硬盤算法
因爲SSD硬盤還處於快速發展階段,因此本文的內容不涉及SSD相關應用系統。sql
根據數據庫知識,咱們能夠列出每種硬件主要的工做內容:數據庫
CPU及內存:緩存數據訪問、比較、排序、事務檢測、SQL解析、函數或邏輯運算;編程
網絡:結果數據傳輸、SQL請求、遠程數據庫訪問(dblink);瀏覽器
硬盤:數據訪問、數據寫入、日誌記錄、大數據量排序、大表鏈接。
根據當前計算機硬件的基本性能指標及其在數據庫中主要操做內容,能夠整理出以下圖所示的性能基本優化法則:
這個優化法則概括爲5個層次:
一、 減小數據訪問(減小磁盤訪問)
二、 返回更少數據(減小網絡傳輸或磁盤訪問)
三、 減小交互次數(減小網絡傳輸)
四、 減小服務器CPU開銷(減小CPU及內存開銷)
五、 利用更多資源(增長資源)
因爲每一層優化法則都是解決其對應硬件的性能問題,因此帶來的性能提高比例也不同。傳統數據庫系統設計是也是儘量對低速設備提供優化方法,所以針對低速設備問題的可優化手段也更多,優化成本也更低。咱們任何一個SQL的性能優化都應該按這個規則由上到下來診斷問題並提出解決方案,而不該該首先想到的是增長資源解決問題。
如下是每一個優化法則層級對應優化效果及成本經驗參考:
優化法則 |
性能提高效果 |
優化成本 |
減小數據訪問 |
1~1000 |
低 |
返回更少數據 |
1~100 |
低 |
減小交互次數 |
1~20 |
低 |
減小服務器CPU開銷 |
1~5 |
低 |
利用更多資源 |
@~10 |
高 |
接下來,咱們針對5種優化法則列舉經常使用的優化手段並結合實例分析。
數據塊是數據庫中數據在磁盤中存儲的最小單位,也是一次IO訪問的最小單位,一個數據塊一般能夠存儲多條記錄,數據塊大小是DBA在建立數據庫或表空間時指定,可指定爲2K、4K、8K、16K或32K字節。下圖是一個Oracle數據庫典型的物理結構,一個數據庫能夠包括多個數據文件,一個數據文件內又包含多個數據塊;
ROWID是每條記錄在數據庫中的惟一標識,經過ROWID能夠直接定位記錄到對應的文件號及數據塊位置。ROWID內容包括文件號、對像號、數據塊號、記錄槽號,以下圖所示:
數據庫索引的原理很是簡單,但在複雜的表中真正能正確使用索引的人不多,即便是專業的DBA也不必定能徹底作到最優。
索引會大大增長表記錄的DML(INSERT,UPDATE,DELETE)開銷,正確的索引可讓性能提高100,1000倍以上,不合理的索引也可能會讓性能降低100倍,所以在一個表中建立什麼樣的索引須要平衡各類業務需求。
索引常見問題:
索引有哪些種類?
常見的索引有B-TREE索引、位圖索引、全文索引,位圖索引通常用於數據倉庫應用,全文索引因爲使用較少,這裏不深刻介紹。B-TREE索引包括不少擴展類型,如組合索引、反向索引、函數索引等等,如下是B-TREE索引的簡單介紹:
B-TREE索引也稱爲平衡樹索引(Balance Tree),它是一種按字段排好序的樹形目錄結構,主要用於提高查詢性能和惟一約束支持。B-TREE索引的內容包括根節點、分支節點、葉子節點。
葉子節點內容:索引字段內容+表記錄ROWID
根節點,分支節點內容:當一個數據塊中不能放下全部索引字段數據時,就會造成樹形的根節點或分支節點,根節點與分支節點保存了索引樹的順序及各層級間的引用關係。
一個普通的BTREE索引結構示意圖以下所示:
若是咱們把一個表的內容認爲是一本字典,那索引就至關於字典的目錄,以下圖所示:
圖中是一個字典按部首+筆劃數的目錄,至關於給字典建了一個按部首+筆劃的組合索引。
一個表中能夠建多個索引,就如一本字典能夠建多個目錄同樣(按拼音、筆劃、部首等等)。
一個索引也能夠由多個字段組成,稱爲組合索引,如上圖就是一個按部首+筆劃的組合目錄。
SQL什麼條件會使用索引?
當字段上建有索引時,一般如下狀況會使用索引:
INDEX_COLUMN = ?
INDEX_COLUMN > ?
INDEX_COLUMN >= ?
INDEX_COLUMN < ?
INDEX_COLUMN <= ?
INDEX_COLUMN between ? and ?
INDEX_COLUMN in (?,?,...,?)
INDEX_COLUMN like ?||'%'(後導模糊查詢)
T1. INDEX_COLUMN=T2. COLUMN1(兩個表經過索引字段關聯)
SQL什麼條件不會使用索引?
查詢條件 |
不能使用索引緣由 |
INDEX_COLUMN <> ? INDEX_COLUMN not in (?,?,...,?) |
不等於操做不能使用索引 |
function(INDEX_COLUMN) = ? INDEX_COLUMN + 1 = ? INDEX_COLUMN || 'a' = ? |
通過普通運算或函數運算後的索引字段不能使用索引 |
INDEX_COLUMN like '%'||? INDEX_COLUMN like '%'||?||'%' |
含前導模糊查詢的Like語法不能使用索引 |
INDEX_COLUMN is null |
B-TREE索引裏不保存字段爲NULL值記錄,所以IS NULL不能使用索引 |
NUMBER_INDEX_COLUMN='12345' CHAR_INDEX_COLUMN=12345 |
Oracle在作數值比較時須要將兩邊的數據轉換成同一種數據類型,若是兩邊數據類型不一樣時會對字段值隱式轉換,至關於加了一層函數處理,因此不能使用索引。 |
a.INDEX_COLUMN=a.COLUMN_1 |
給索引查詢的值應是已知數據,不能是未知字段值。 |
注: 通過函數運算字段的字段要使用能夠使用函數索引,這種需求建議與DBA溝通。 有時候咱們會使用多個字段的組合索引,若是查詢條件中第一個字段不能使用索引,那整個查詢也不能使用索引 如:咱們company表建了一個id+name的組合索引,如下SQL是不能使用索引的 Select * from company where name=? Oracle9i後引入了一種index skip scan的索引方式來解決相似的問題,可是經過index skip scan提升性能的條件比較特殊,使用很差反而性能會更差。
|
咱們通常在什麼字段上建索引?
這是一個很是複雜的話題,須要對業務及數據充分分析後再能得出結果。主鍵及外鍵一般都要有索引,其它須要建索引的字段應知足如下條件:
一、字段出如今查詢條件中,而且查詢條件能夠使用索引;
二、語句執行頻率高,一天會有幾千次以上;
三、經過字段條件可篩選的記錄集很小,那數據篩選比例是多少才適合?
這個沒有固定值,須要根據表數據量來評估,如下是經驗公式,可用於快速評估:
小表(記錄數小於10000行的表):篩選比例<10%;
大表:(篩選返回記錄數)<(表總記錄數*單條記錄長度)/10000/16
單條記錄長度≈字段平均內容長度之和+字段數*2
如下是一些字段是否須要建B-TREE索引的經驗分類:
|
字段類型 |
常見字段名 |
須要建索引的字段 |
主鍵 |
ID,PK |
外鍵 |
PRODUCT_ID,COMPANY_ID,MEMBER_ID,ORDER_ID,TRADE_ID,PAY_ID |
|
有對像或身份標識意義字段 |
HASH_CODE,USERNAME,IDCARD_NO,EMAIL,TEL_NO,IM_NO |
|
索引慎用字段,須要進行數據分佈及使用場景詳細評估 |
日期 |
GMT_CREATE,GMT_MODIFIED |
年月 |
YEAR,MONTH |
|
狀態標誌 |
PRODUCT_STATUS,ORDER_STATUS,IS_DELETE,VIP_FLAG |
|
類型 |
ORDER_TYPE,IMAGE_TYPE,GENDER,CURRENCY_TYPE |
|
區域 |
COUNTRY,PROVINCE,CITY |
|
操做人員 |
CREATOR,AUDITOR |
|
數值 |
LEVEL,AMOUNT,SCORE |
|
長字符 |
ADDRESS,COMPANY_NAME,SUMMARY,SUBJECT |
|
不適合建索引的字段 |
描述備註 |
DESCRIPTION,REMARK,MEMO,DETAIL |
大字段 |
FILE_CONTENT,EMAIL_CONTENT |
如何知道SQL是否使用了正確的索引?
簡單SQL能夠根據索引使用語法規則判斷,複雜的SQL很差辦,判斷SQL的響應時間是一種策略,可是這會受到數據量、主機負載及緩存等因素的影響,有時數據全在緩存裏,可能全表訪問的時間比索引訪問時間還少。要準確知道索引是否正確使用,須要到數據庫中查看SQL真實的執行計劃,這個話題比較複雜,詳見SQL執行計劃專題介紹。
索引對DML(INSERT,UPDATE,DELETE)附加的開銷有多少?
這個沒有固定的比例,與每一個表記錄的大小及索引字段大小密切相關,如下是一個普通表測試數據,僅供參考:
索引對於Insert性能下降56%
索引對於Update性能下降47%
索引對於Delete性能下降29%
所以對於寫IO壓力比較大的系統,表的索引須要仔細評估必要性,另外索引也會佔用必定的存儲空間。
有些時候,咱們只是訪問表中的幾個字段,而且字段內容較少,咱們能夠爲這幾個字段單獨創建一個組合索引,這樣就能夠直接只經過訪問索引就能獲得數據,通常索引佔用的磁盤空間比表小不少,因此這種方式能夠大大減小磁盤IO開銷。
如:select id,name from company where type='2';
若是這個SQL常用,咱們能夠在type,id,name上建立組合索引
create index my_comb_index on company(type,id,name);
有了這個組合索引後,SQL就能夠直接經過my_comb_index索引返回數據,不須要訪問company表。
仍是拿字典舉例:有一個需求,須要查詢一本漢語字典中全部漢字的個數,若是咱們的字典沒有目錄索引,那咱們只能從字典內容裏一個一個字計數,最後返回結果。若是咱們有一個拼音目錄,那就能夠只訪問拼音目錄的漢字進行計數。若是一本字典有1000頁,拼音目錄有20頁,那咱們的數據訪問成本至關於全表訪問的50分之一。
切記,性能優化是無止境的,當性能能夠知足需求時便可,不要過分優化。在實際數據庫中咱們不可能把每一個SQL請求的字段都建在索引裏,因此這種只經過索引訪問數據的方法通常只用於核心應用,也就是那種對核心表訪問量最高且查詢字段數據量不多的查詢。
SQL執行計劃是關係型數據庫最核心的技術之一,它表示SQL執行時的數據訪問算法。因爲業務需求愈來愈複雜,表數據量也愈來愈大,程序員愈來愈懶惰,SQL也須要支持很是複雜的業務邏輯,但SQL的性能還須要提升,所以,優秀的關係型數據庫除了須要支持複雜的SQL語法及更多函數外,還須要有一套優秀的算法庫來提升SQL性能。
目前ORACLE有SQL執行計劃的算法約300種,並且一直在增長,因此SQL執行計劃是一個很是複雜的課題,一個普通DBA能掌握50種就很不錯了,就算是資深DBA也不可能把每一個執行計劃的算法描述清楚。雖然有這麼多種算法,但並不表示咱們沒法優化執行計劃,由於咱們經常使用的SQL執行計劃算法也就十幾個,若是一個程序員能把這十幾個算法搞清楚,那就掌握了80%的SQL執行計劃調優知識。
因爲篇幅的緣由,SQL執行計劃須要專題介紹,在這裏就很少說了。
通常數據分頁方式有:
2.1.一、客戶端(應用程序或瀏覽器)分頁
將數據從應用服務器所有下載到本地應用程序或瀏覽器,在應用程序或瀏覽器內部經過本地代碼進行分頁處理
優勢:編碼簡單,減小客戶端與應用服務器網絡交互次數
缺點:首次交互時間長,佔用客戶端內存
適應場景:客戶端與應用服務器網絡延時較大,但要求後續操做流暢,如手機GPRS,超遠程訪問(跨國)等等。
2.1.二、應用服務器分頁
將數據從數據庫服務器所有下載到應用服務器,在應用服務器內部再進行數據篩選。如下是一個應用服務器端Java程序分頁的示例:
List list=executeQuery(「select * from employee order by id」);
Int count= list.size();
List subList= list.subList(10, 20);
優勢:編碼簡單,只須要一次SQL交互,總數據與分頁數據差很少時性能較好。
缺點:總數據量較多時性能較差。
適應場景:數據庫系統不支持分頁處理,數據量較小而且可控。
2.1.三、數據庫SQL分頁
採用數據庫SQL分頁須要兩次SQL完成
一個SQL計算總數量
一個SQL返回分頁後的數據
優勢:性能好
缺點:編碼複雜,各類數據庫語法不一樣,須要兩次SQL交互。
oracle數據庫通常採用rownum來進行分頁,經常使用分頁語法有以下兩種:
直接經過rownum分頁:
select * from (
select a.*,rownum rn from
(select * from product a where company_id=? order by status) a
where rownum<=20)
where rn>10;
數據訪問開銷=索引IO+索引所有記錄結果對應的表數據IO
採用rowid分頁語法
優化原理是經過純索引找出分頁記錄的ROWID,再經過ROWID回表返回數據,要求內層查詢和排序字段全在索引裏。
create index myindex on product(company_id,status);
select b.* from (
select * from (
select a.*,rownum rn from
(select rowid rid,status from product a where company_id=? order by status) a
where rownum<=20)
where rn>10) a, product b
where a.rid=b.rowid;
數據訪問開銷=索引IO+索引分頁結果對應的表數據IO
實例:
一個公司產品有1000條記錄,要分頁取其中20個產品,假設訪問公司索引須要50個IO,2條記錄須要1個表數據IO。
那麼按第一種ROWNUM分頁寫法,須要550(50+1000/2)個IO,按第二種ROWID分頁寫法,只須要60個IO(50+20/2);
經過去除沒必要要的返回字段能夠提升性能,例:
調整前:select * from product where company_id=?;
調整後:select id,name from product where company_id=?;
優勢:
一、減小數據在網絡上傳輸開銷
二、減小服務器數據處理開銷
三、減小客戶端內存佔用
四、字段變動時提早發現問題,減小程序BUG
五、若是訪問的全部字段恰好在一個索引裏面,則能夠使用純索引訪問提升性能。
缺點:增長編碼工做量
因爲會增長一些編碼工做量,因此通常需求經過開發規範來要求程序員這麼作,不然等項目上線後再整改工做量更大。
若是你的查詢表中有大字段或內容較多的字段,如備註信息、文件內容等等,那在查詢表時必定要注意這方面的問題,不然可能會帶來嚴重的性能問題。若是表常常要查詢而且請求大內容字段的機率很低,咱們能夠採用分表處理,將一個大表分拆成兩個一對一的關係表,將不經常使用的大內容字段放在一張單獨的表中。如一張存儲上傳文件的表:
T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE,FILE_CONTENT)
咱們能夠分拆成兩張一對一的關係表:
T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE)
T_FILECONTENT(ID, FILE_CONTENT)
經過這種分拆,能夠大大提少T_FILE表的單條記錄及總大小,這樣在查詢T_FILE時性能會更好,當須要查詢FILE_CONTENT字段內容時再訪問T_FILECONTENT表。
數據庫訪問框架通常都提供了批量提交的接口,jdbc支持batch的提交處理方法,當你一次性要往一個表中插入1000萬條數據時,若是採用普通的executeUpdate處理,那麼和服務器交互次數爲1000萬次,按每秒鐘能夠向數據庫服務器提交10000次估算,要完成全部工做須要1000秒。若是採用批量提交模式,1000條提交一次,那麼和服務器交互次數爲1萬次,交互次數大大減小。採用batch操做通常不會減小不少數據庫服務器的物理IO,可是會大大減小客戶端與服務端的交互次數,從而減小了屢次發起的網絡延時開銷,同時也會下降數據庫的CPU開銷。
假設要向一個普通表插入1000萬數據,每條記錄大小爲1K字節,表上沒有任何索引,客戶端與數據庫服務器網絡是100Mbps,如下是根據如今通常計算機能力估算的各類batch大小性能對比值:
單位:ms |
No batch |
Batch=10 |
Batch=100 |
Batch=1000 |
Batch=10000 |
服務器事務處理時間 |
0.1 |
0.1 |
0.1 |
0.1 |
0.1 |
服務器IO處理時間 |
0.02 |
0.2 |
2 |
20 |
200 |
網絡交互發起時間 |
0.1 |
0.1 |
0.1 |
0.1 |
0.1 |
網絡數據傳輸時間 |
0.01 |
0.1 |
1 |
10 |
100 |
小計 |
0.23 |
0.5 |
3.2 |
30.2 |
300.2 |
平均每條記錄處理時間 |
0.23 |
0.05 |
0.032 |
0.0302 |
0.03002 |
從上能夠看出,Insert操做加大Batch能夠對性能提升近8倍性能,通常根據主鍵的Update或Delete操做也可能提升2-3倍性能,但不如Insert明顯,由於Update及Delete操做可能有比較大的開銷在物理IO訪問。以上僅是理論計算值,實際狀況須要根據具體環境測量。
不少時候咱們須要按一些ID查詢數據庫記錄,咱們能夠採用一個ID一個請求發給數據庫,以下所示:
for :var in ids[] do begin
select * from mytable where id=:var;
end;
咱們也能夠作一個小的優化, 以下所示,用ID INLIST的這種方式寫SQL:
select * from mytable where id in(:id1,id2,...,idn);
經過這樣處理能夠大大減小SQL請求的數量,從而提升性能。那若是有10000個ID,那是否是所有放在一條SQL裏處理呢?答案確定是否認的。首先大部份數據庫都會有SQL長度和IN裏個數的限制,如ORACLE的IN裏就不容許超過1000個值。
另外當前數據庫通常都是採用基於成本的優化規則,當IN數量達到必定值時有可能改變SQL執行計劃,從索引訪問變成全表訪問,這將使性能急劇變化。隨着SQL中IN的裏面的值個數增長,SQL的執行計劃會更復雜,佔用的內存將會變大,這將會增長服務器CPU及內存成本。
評估在IN裏面一次放多少個值還須要考慮應用服務器本地內存的開銷,有併發訪問時要計算本地數據使用週期內的併發上限,不然可能會致使內存溢出。
綜合考慮,通常IN裏面的值個數超過20個之後性能基本沒什麼太大變化,也特別說明不要超過100,超事後可能會引發執行計劃的不穩定性及增長數據庫CPU及內存成本,這個須要專業DBA評估。
當咱們採用select從數據庫查詢數據時,數據默認並非一條一條返回給客戶端的,也不是一次所有返回客戶端的,而是根據客戶端fetch_size參數處理,每次只返回fetch_size條記錄,當客戶端遊標遍歷到尾部時再從服務端取數據,直到最後所有傳送完成。因此若是咱們要從服務端一次取大量數據時,能夠加大fetch_size,這樣能夠減小結果數據傳輸的交互次數及服務器數據準備時間,提升性能。
如下是jdbc測試的代碼,採用本地數據庫,表緩存在數據庫CACHE中,所以沒有網絡鏈接及磁盤IO開銷,客戶端只遍歷遊標,不作任何處理,這樣更能體現fetch參數的影響:
String vsql ="select * from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(1000);
ResultSet rs = pstmt.executeQuery(vsql);
int cnt = rs.getMetaData().getColumnCount();
Object o;
while (rs.next()) {
for (int i = 1; i <= cnt; i++) {
o = rs.getObject(i);
}
}
測試示例中的employee表有100000條記錄,每條記錄平均長度135字節
如下是測試結果,對每種fetchsize測試5次再取平均值:
fetchsize |
elapse_time(s) |
1 |
20.516 |
2 |
11.34 |
4 |
6.894 |
8 |
4.65 |
16 |
3.584 |
32 |
2.865 |
64 |
2.656 |
128 |
2.44 |
256 |
2.765 |
512 |
3.075 |
1024 |
2.862 |
2048 |
2.722 |
4096 |
2.681 |
8192 |
2.715 |
Oracle jdbc fetchsize默認值爲10,由上測試能夠看出fetchsize對性能影響仍是比較大的,可是當fetchsize大於100時就基本上沒有影響了。fetchsize並不會存在一個最優的固定值,由於總體性能與記錄集大小及硬件平臺有關。根據測試結果建議當一次性要取大量數據時這個值設置爲100左右,不要小於40。注意,fetchsize不能設置太大,若是一次取出的數據大於JVM的內存會致使內存溢出,因此建議不要超過1000,太大了也沒什麼性能提升,反而可能會增長內存溢出的危險。
注:圖中fetchsize在128之後會有一些小的波動,這並非測試偏差,而是因爲resultset填充到具體對像時間不一樣的緣由,因爲resultset已經到本地內存裏了,因此估計是因爲CPU的L1,L2 Cache命中率變化形成,因爲變化不大,因此筆者也未深刻分析緣由。
iBatis的SqlMapping配置文件能夠對每一個SQL語句指定fetchsize大小,以下所示:
<select id="getAllProduct" resultMap="HashMap" fetchSize="1000">
select * from employee
</select>
大型數據庫通常都支持存儲過程,合理的利用存儲過程也能夠提升系統性能。如你有一個業務須要將A表的數據作一些加工而後更新到B表中,可是又不可能一條SQL完成,這時你須要以下3步操做:
a:將A表數據所有取出到客戶端;
b:計算出要更新的數據;
c:將計算結果更新到B表。
若是採用存儲過程你能夠將整個業務邏輯封裝在存儲過程裏,而後在客戶端直接調用存儲過程處理,這樣能夠減小網絡交互的成本。
固然,存儲過程也並非十全十美,存儲過程有如下缺點:
a、不可移植性,每種數據庫的內部編程語法都不太相同,當你的系統須要兼容多種數據庫時最好不要用存儲過程。
b、學習成本高,DBA通常都擅長寫存儲過程,但並非每一個程序員都能寫好存儲過程,除非你的團隊有較多的開發人員熟悉寫存儲過程,不然後期系統維護會產生問題。
c、業務邏輯多處存在,採用存儲過程後也就意味着你的系統有一些業務邏輯不是在應用程序裏處理,這種架構會增長一些系統維護和調試成本。
d、存儲過程和經常使用應用程序語言不同,它支持的函數及語法有可能不能知足需求,有些邏輯就只能經過應用程序處理。
e、若是存儲過程當中有複雜運算的話,會增長一些數據庫服務端的處理成本,對於集中式數據庫可能會致使系統可擴展性問題。
f、爲了提升性能,數據庫會把存儲過程代碼編譯成中間運行代碼(相似於java的class文件),因此更像靜態語言。當存儲過程引用的對像(表、視圖等等)結構改變後,存儲過程須要從新編譯才能生效,在24*7高併發應用場景,通常都是在線變動結構的,因此在變動的瞬間要同時編譯存儲過程,這可能會致使數據庫瞬間壓力上升引發故障(Oracle數據庫就存在這樣的問題)。
我的觀點:普通業務邏輯儘可能不要使用存儲過程,定時性的ETL任務或報表統計函數能夠根據團隊資源狀況採用存儲過程處理。
要經過優化業務邏輯來提升性能是比較困難的,這須要程序員對所訪問的數據及業務流程很是清楚。
舉一個案例:
某移動公司推出優惠套參,活動對像爲VIP會員而且2010年1,2,3月平均話費20元以上的客戶。
那咱們的檢測邏輯爲:
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
select vip_flag from member where phone_no='13988888888';
if avg_money>20 and vip_flag=true then
begin
執行套參();
end;
若是咱們修改業務邏輯爲:
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
if avg_money>20 then
begin
select vip_flag from member where phone_no='13988888888';
if vip_flag=true then
begin
執行套參();
end;
end;
經過這樣能夠減小一些判斷vip_flag的開銷,平均話費20元如下的用戶就不須要再檢測是否VIP了。
若是程序員分析業務,VIP會員比例爲1%,平均話費20元以上的用戶比例爲90%,那咱們改爲以下:
select vip_flag from member where phone_no='13988888888';
if vip_flag=true then
begin
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
if avg_money>20 then
begin
執行套參();
end;
end;
這樣就只有1%的VIP會員纔會作檢測平均話費,最終大大減小了SQL的交互次數。
以上只是一個簡單的示例,實際的業務老是比這複雜得多,因此通常只是高級程序員更容易作出優化的邏輯,可是咱們須要有這樣一種成本優化的意識。
如今大部分Java框架都是經過jdbc從數據庫取出數據,而後裝載到一個list裏再處理,list裏多是業務Object,也多是hashmap。
因爲JVM內存通常都小於4G,因此不可能一次經過sql把大量數據裝載到list裏。爲了完成功能,不少程序員喜歡採用分頁的方法處理,如一次從數據庫取1000條記錄,經過屢次循環搞定,保證不會引發JVM Out of memory問題。
如下是實現此功能的代碼示例,t_employee表有10萬條記錄,設置分頁大小爲1000:
d1 = Calendar.getInstance().getTime();
vsql = "select count(*) cnt from t_employee";
pstmt = conn.prepareStatement(vsql);
ResultSet rs = pstmt.executeQuery();
Integer cnt = 0;
while (rs.next()) {
cnt = rs.getInt("cnt");
}
Integer lastid=0;
Integer pagesize=1000;
System.out.println("cnt:" + cnt);
String vsql = "select count(*) cnt from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql);
ResultSet rs = pstmt.executeQuery();
Integer cnt = 0;
while (rs.next()) {
cnt = rs.getInt("cnt");
}
Integer lastid = 0;
Integer pagesize = 1000;
System.out.println("cnt:" + cnt);
for (int i = 0; i <= cnt / pagesize; i++) {
vsql = "select * from (select * from t_employee where id>? order by id) where rownum<=?";
pstmt = conn.prepareStatement(vsql);
pstmt.setFetchSize(1000);
pstmt.setInt(1, lastid);
pstmt.setInt(2, pagesize);
rs = pstmt.executeQuery();
int col_cnt = rs.getMetaData().getColumnCount();
Object o;
while (rs.next()) {
for (int j = 1; j <= col_cnt; j++) {
o = rs.getObject(j);
}
lastid = rs.getInt("id");
}
rs.close();
pstmt.close();
}
以上代碼實際執行時間爲6.516秒
不少持久層框架爲了儘可能讓程序員使用方便,封裝了jdbc經過statement執行數據返回到resultset的細節,致使程序員會想採用分頁的方式處理問題。實際上若是咱們採用jdbc原始的resultset遊標處理記錄,在resultset循環讀取的過程當中處理記錄,這樣就能夠一次從數據庫取出全部記錄。顯著提升性能。
這裏須要注意的是,採用resultset遊標處理記錄時,應該將遊標的打開方式設置爲FORWARD_READONLY模式(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY),不然會把結果緩存在JVM裏,形成JVM Out of memory問題。
代碼示例:
String vsql ="select * from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(100);
ResultSet rs = pstmt.executeQuery(vsql);
int col_cnt = rs.getMetaData().getColumnCount();
Object o;
while (rs.next()) {
for (int j = 1; j <= col_cnt; j++) {
o = rs.getObject(j);
}
}
調整後的代碼實際執行時間爲3.156秒
從測試結果能夠看出性能提升了1倍多,若是採用分頁模式數據庫每次還需發生磁盤IO的話那性能能夠提升更多。
iBatis等持久層框架考慮到會有這種需求,因此也有相應的解決方案,在iBatis裏咱們不能採用queryForList的方法,而應用該採用queryWithRowHandler加回調事件的方式處理,以下所示:
MyRowHandler myrh=new MyRowHandler();
sqlmap.queryWithRowHandler("getAllEmployee", myrh);
class MyRowHandler implements RowHandler {
public void handleRow(Object o) {
//todo something
}
}
iBatis的queryWithRowHandler很好的封裝了resultset遍歷的事件處理,效果及性能與resultset遍歷同樣,也不會產生JVM內存溢出。
綁定變量是指SQL中對變化的值採用變量參數的形式提交,而不是在SQL中直接拼寫對應的值。
非綁定變量寫法:Select * from employee where id=1234567
綁定變量寫法:
Select * from employee where id=?
Preparestatement.setInt(1,1234567)
Java中Preparestatement就是爲處理綁定變量提供的對像,綁定變量有如下優勢:
一、防止SQL注入
二、提升SQL可讀性
三、提升SQL解析性能,不使用綁定變動咱們通常稱爲硬解析,使用綁定變量咱們稱爲軟解析。
第1和第2點很好理解,作編碼的人應該都清楚,這裏不詳細說明。關於第3點,到底能提升多少性能呢,下面舉一個例子說明:
假設有這個這樣的一個數據庫主機:
2個4核CPU
100塊磁盤,每一個磁盤支持IOPS爲160
業務應用的SQL以下:
select * from table where pk=?
這個SQL平均4個IO(3個索引IO+1個數據IO)
IO緩存命中率75%(索引全在內存中,數據須要訪問磁盤)
SQL硬解析CPU消耗:1ms (經常使用經驗值)
SQL軟解析CPU消耗:0.02ms(經常使用經驗值)
假設CPU每核性能是線性增加,訪問內存Cache中的IO時間忽略,要求計算系統對如上應用採用硬解析與採用軟解析支持的每秒最大併發數:
是否使用綁定變量 |
CPU支持最大併發數 |
磁盤IO支持最大併發數 |
不使用 |
2*4*1000=8000 |
100*160=16000 |
使用 |
2*4*1000/0.02=400000 |
100*160=16000 |
從以上計算能夠看出,不使用綁定變量的系統當併發達到8000時會在CPU上產生瓶頸,當使用綁定變量的系統當並行達到16000時會在磁盤IO上產生瓶頸。因此若是你的系統CPU有瓶頸時請先檢查是否存在大量的硬解析操做。
使用綁定變量爲什麼會提升SQL解析性能,這個須要從數據庫SQL執行原理說明,一條SQL在Oracle數據庫中的執行過程以下圖所示:
當一條SQL發送給數據庫服務器後,系統首先會將SQL字符串進行hash運算,獲得hash值後再從服務器內存裏的SQL緩存區中進行檢索,若是有相同的SQL字符,而且確認是同一邏輯的SQL語句,則從共享池緩存中取出SQL對應的執行計劃,根據執行計劃讀取數據並返回結果給客戶端。
若是在共享池中未發現相同的SQL則根據SQL邏輯生成一條新的執行計劃並保存在SQL緩存區中,而後根據執行計劃讀取數據並返回結果給客戶端。
爲了更快的檢索SQL是否在緩存區中,首先進行的是SQL字符串hash值對比,若是未找到則認爲沒有緩存,若是存在再進行下一步的準確對比,因此要命中SQL緩存區應保證SQL字符是徹底一致,中間有大小寫或空格都會認爲是不一樣的SQL。
若是咱們不採用綁定變量,採用字符串拼接的模式生成SQL,那麼每條SQL都會產生執行計劃,這樣會致使共享池耗盡,緩存命中率也很低。
一些不使用綁定變量的場景:
a、數據倉庫應用,這種應用通常併發不高,可是每一個SQL執行時間很長,SQL解析的時間相比SQL執行時間比較小,綁定變量對性能提升不明顯。數據倉庫通常都是內部分析應用,因此也不太會發生SQL注入的安全問題。
b、數據分佈不均勻的特殊邏輯,如產品表,記錄有1億,有一產品狀態字段,上面建有索引,有審覈中,審覈經過,審覈未經過3種狀態,其中審覈經過9500萬,審覈中1萬,審覈不經過499萬。
要作這樣一個查詢:
select count(*) from product where status=?
採用綁定變量的話,那麼只會有一個執行計劃,若是走索引訪問,那麼對於審覈中查詢很快,對審覈經過和審覈不經過會很慢;若是不走索引,那麼對於審覈中與審覈經過和審覈不經過時間基本同樣;
對於這種狀況應該不使用綁定變量,而直接採用字符拼接的方式生成SQL,這樣能夠爲每一個SQL生成不一樣的執行計劃,以下所示。
select count(*) from product where status='approved'; //不使用索引
select count(*) from product where status='tbd'; //不使用索引
select count(*) from product where status='auditing';//使用索引
Oracle的排序算法一直在優化,可是整體時間複雜度約等於nLog(n)。普通OLTP系統排序操做通常都是在內存裏進行的,對於數據庫來講是一種CPU的消耗,曾在PC機作過測試,單核普通CPU在1秒鐘能夠完成100萬條記錄的全內存排序操做,因此說因爲如今CPU的性能加強,對於普通的幾十條或上百條記錄排序對系統的影響也不會很大。可是當你的記錄集增長到上萬條以上時,你須要注意是否必定要這麼作了,大記錄集排序不只增長了CPU開銷,並且可能會因爲內存不足發生硬盤排序的現象,當發生硬盤排序時性能會急劇降低,這種需求須要與DBA溝通再決定,取決於你的需求和數據,因此只有你本身最清楚,而不要被別人說排序很慢就嚇倒。
如下列出了可能會發生排序操做的SQL語法:
Order by
Group by
Distinct
Exists子查詢
Not Exists子查詢
In子查詢
Not In子查詢
Union(並集),Union All也是一種並集操做,可是不會發生排序,若是你確認兩個數據集不須要執行去除重複數據操做,那請使用Union All 代替Union。
Minus(差集)
Intersect(交集)
Create Index
Merge Join,這是一種兩個錶鏈接的內部算法,執行時會把兩個表先排序好再鏈接,應用於兩個大表鏈接的操做。若是你的兩個錶鏈接的條件都是等值運算,那能夠採用Hash Join來提升性能,由於Hash Join使用Hash 運算來代替排序的操做。具體原理及設置參考SQL執行計劃優化專題。
咱們SQL的業務邏輯常常會包含一些比較操做,如a=b,a<b之類的操做,對於這些比較操做數據庫都體現得很好,可是若是有如下操做,咱們須要保持警戒:
Like模糊查詢,以下所示:
a like ‘%abc%’
Like模糊查詢對於數據庫來講不是很擅長,特別是你須要模糊檢查的記錄有上萬條以上時,性能比較糟糕,這種狀況通常能夠採用專用Search或者採用全文索引方案來提升性能。
不能使用索引定位的大量In List,以下所示:
a in (:1,:2,:3,…,:n) ----n>20
若是這裏的a字段不能經過索引比較,那數據庫會將字段與in裏面的每一個值都進行比較運算,若是記錄數有上萬以上,會明顯感受到SQL的CPU開銷加大,這個狀況有兩種解決方式:
a、 將in列表裏面的數據放入一張中間小表,採用兩個表Hash Join關聯的方式處理;
b、 採用str2varList方法將字段串列表轉換一個臨時表處理,關於str2varList方法能夠在網上直接查詢,這裏不詳細介紹。
以上兩種解決方案都須要與中間表Hash Join的方式才能提升性能,若是採用了Nested Loop的鏈接方式性能會更差。
若是發現咱們的系統IO沒問題可是CPU負載很高,就有多是上面的緣由,這種狀況不太常見,若是遇到了最好能和DBA溝通並確認準確的緣由。
什麼是複雜運算,通常我認爲是一秒鐘CPU只能作10萬次之內的運算。如含小數的對數及指數運算、三角函數、3DES及BASE64數據加密算法等等。
若是有大量這類函數運算,儘可能放在客戶端處理,通常CPU每秒中也只能處理1萬-10萬次這樣的函數運算,放在數據庫內不利於高併發處理。
多進程並行訪問是指在客戶端建立多個進程(線程),每一個進程創建一個與數據庫的鏈接,而後同時向數據庫提交訪問請求。當數據庫主機資源有空閒時,咱們能夠採用客戶端多進程並行訪問的方法來提升性能。若是數據庫主機已經很忙時,採用多進程並行訪問性能不會提升,反而可能會更慢。因此使用這種方式最好與DBA或系統管理員進行溝通後再決定是否採用。
例如:
咱們有10000個產品ID,如今須要根據ID取出產品的詳細信息,若是單線程訪問,按每一個IO要5ms計算,忽略主機CPU運算及網絡傳輸時間,咱們須要50s才能完成任務。若是採用5個並行訪問,每一個進程訪問2000個ID,那麼10s就有可能完成任務。
那是否是並行數越多越好呢,開1000個並行是否只要50ms就搞定,答案確定是否認的,當並行數超過服務器主機資源的上限時性能就不會再提升,若是再增長反而會增長主機的進程間調度成本和進程衝突機率。
如下是一些如何設置並行數的基本建議:
若是瓶頸在服務器主機,可是主機還有空閒資源,那麼最大並行數取主機CPU核數和主機提供數據服務的磁盤數兩個參數中的最小值,同時要保證主機有資源作其它任務。
若是瓶頸在客戶端處理,可是客戶端還有空閒資源,那建議不要增長SQL的並行,而是用一個進程取回數據後在客戶端起多個進程處理便可,進程數根據客戶端CPU核數計算。
若是瓶頸在客戶端網絡,那建議作數據壓縮或者增長多個客戶端,採用map reduce的架構處理。
若是瓶頸在服務器網絡,那須要增長服務器的網絡帶寬或者在服務端將數據壓縮後再處理了。
數據庫並行處理是指客戶端一條SQL的請求,數據庫內部自動分解成多個進程並行處理,以下圖所示:
並非全部的SQL均可以使用並行處理,通常只有對錶或索引進行所有訪問時才能夠使用並行。數據庫表默認是不打開並行訪問,因此須要指定SQL並行的提示,以下所示:
select /*+parallel(a,4)*/ * from employee;
並行的優勢:
使用多進程處理,充分利用數據庫主機資源(CPU,IO),提升性能。
並行的缺點:
一、單個會話佔用大量資源,影響其它會話,因此只適合在主機負載低時期使用;
二、只能採用直接IO訪問,不能利用緩存數據,因此執行前會觸發將髒緩存數據寫入磁盤操做。
注:
一、並行處理在OLTP類系統中慎用,使用不當會致使一個會話把主機資源所有佔用,而正常事務得不到及時響應,因此通常只是用於數據倉庫平臺。
二、通常對於百萬級記錄如下的小表採用並行訪問性能並不能提升,反而可能會讓性能更差。
今天面試,我簡歷上寫了熟悉sql的性能優化,可是今天面試,一時想不起別的,就僅僅說出了一條,在這裏再總結一些,完善本身的知識點。
我常常用的數據庫是oracle,因此個人sql優化是程序員針對於oracle的。
總結,這個sql優化是針對程序員的,而不是針對dba的,主要就是第一,儘可能防止模糊,明確指出,即用列名代替*,第二,在where語句上下工夫。第三多表查詢和子查詢,第四儘可能使用綁定。
1、問題的提出
在應用系統開發初期,因爲開發數據庫數據比較少,對於查詢SQL語句,複雜視圖的的編寫等體會不出SQL語句各類寫法的性能優劣,可是若是將應用系統提交實際應用後,隨着數據庫中數據的增長,系統的響應速度就成爲目前系統須要解決的最主要的問題之一。系統優化中一個很重要的方面就是SQL語句的優化。對於海量數據,劣質SQL語句和優質SQL語句之間的速度差異能夠達到上百倍,可見對於一個系統不是簡單地能實現其功能就可,而是要寫出高質量的SQL語句,提升系統的可用性。
在多數狀況下,Oracle使用索引來更快地遍歷表,優化器主要根據定義的索引來提升性能。可是,若是在SQL語句的where子句中寫的SQL代碼不合理,就會形成優化器刪去索引而使用全表掃描,通常就這種SQL語句就是所謂的劣質SQL語句。在編寫SQL語句時咱們應清楚優化器根據何種原則來刪除索引,這有助於寫出高性能的SQL語句。
2、SQL語句編寫注意問題
下面就某些SQL語句的where子句編寫中須要注意的問題做詳細介紹。在這些where子句中,即便某些列存在索引,可是因爲編寫了劣質的SQL,系統在運行該SQL語句時也不能使用該索引,而一樣使用全表掃描,這就形成了響應速度的極大下降。
1. 操做符優化
(a) IN 操做符
用IN寫出來的SQL的優勢是比較容易寫及清晰易懂,這比較適合現代軟件開發的風格。可是用IN的SQL性能老是比較低的,從Oracle執行的步驟來分析用IN的SQL與不用IN的SQL有如下區別:
ORACLE試圖將其轉換成多個表的鏈接,若是轉換不成功則先執行IN裏面的子查詢,再查詢外層的表記錄,若是轉換成功則直接採用多個表的鏈接方式查詢。因而可知用IN的SQL至少多了一個轉換的過程。通常的SQL均可以轉換成功,但對於含有分組統計等方面的SQL就不能轉換了。
推薦方案:在業務密集的SQL當中儘可能不採用IN操做符,用EXISTS 方案代替。
(b) NOT IN操做符
此操做是強列不推薦使用的,由於它不能應用表的索引。
推薦方案:用NOT EXISTS 方案代替
(c) IS NULL 或IS NOT NULL操做(判斷字段是否爲空)
判斷字段是否爲空通常是不會應用索引的,由於索引是不索引空值的。不能用null做索引,任何包含null值的列都將不會被包含在索引中。即便索引有多列這樣的狀況下,只要這些列中有一列含有null,該列就會從索引中排除。也就是說若是某列存在空值,即便對該列建索引也不會提升性能。任何在where子句中使用is null或is not null的語句優化器是不容許使用索引的。
推薦方案:用其它相同功能的操做運算代替,如:a is not null 改成 a>0 或a>’’等。不容許字段爲空,而用一個缺省值代替空值,如申請中狀態字段不容許爲空,缺省爲申請。
(d) > 及 < 操做符(大於或小於操做符)
大於或小於操做符通常狀況下是不用調整的,由於它有索引就會採用索引查找,但有的狀況下能夠對它進行優化,如一個表有100萬記錄,一個數值型字段A,30萬記錄的A=0,30萬記錄的A=1,39萬記錄的A=2,1萬記錄的A=3。那麼執行A>2與A>=3的效果就有很大的區別了,由於A>2時ORACLE會先找出爲2的記錄索引再進行比較,而A>=3時ORACLE則直接找到=3的記錄索引。
(e) LIKE操做符
LIKE操做符能夠應用通配符查詢,裏面的通配符組合可能達到幾乎是任意的查詢,可是若是用得很差則會產生性能上的問題,如LIKE ‘%5400%’ 這種查詢不會引用索引,而LIKE ‘X5400%’則會引用範圍索引。
一個實際例子:用YW_YHJBQK表中營業編號後面的戶標識號可來查詢營業編號 YY_BH LIKE ‘%5400%’ 這個條件會產生全表掃描,若是改爲YY_BH LIKE ’X5400%’ OR YY_BH LIKE ’B5400%’ 則會利用YY_BH的索引進行兩個範圍的查詢,性能確定大大提升。
帶通配符(%)的like語句:
一樣以上面的例子來看這種狀況。目前的需求是這樣的,要求在職工表中查詢名字中包含cliton的人。能夠採用以下的查詢SQL語句:
select * from employee where last_name like '%cliton%';
這裏因爲通配符(%)在搜尋詞首出現,因此Oracle系統不使用last_name的索引。在不少狀況下可能沒法避免這種狀況,可是必定要心中有底,通配符如此使用會下降查詢速度。然而當通配符出如今字符串其餘位置時,優化器就能利用索引。在下面的查詢中索引獲得了使用:
select * from employee where last_name like 'c%';
(f) UNION操做符
UNION在進行表連接後會篩選掉重複的記錄,因此在表連接後會對所產生的結果集進行排序運算,刪除重複的記錄再返回結果。實際大部分應用中是不會產生重複的記錄,最多見的是過程表與歷史表UNION。如:
select * from gc_dfys
union
select * from ls_jg_dfys
這個SQL在運行時先取出兩個表的結果,再用排序空間進行排序刪除重複的記錄,最後返回結果集,若是表數據量大的話可能會致使用磁盤進行排序。
推薦方案:採用UNION ALL操做符替代UNION,由於UNION ALL操做只是簡單的將兩個結果合併後就返回。
select * from gc_dfys
union all
select * from ls_jg_dfys
(g) 聯接列
對於有聯接的列,即便最後的聯接值爲一個靜態值,優化器是不會使用索引的。咱們一塊兒來看一個例子,假定有一個職工表(employee),對於一個職工的姓和名分紅兩列存放(FIRST_NAME和LAST_NAME),如今要查詢一個叫比爾.克林頓(Bill Cliton)的職工。
下面是一個採用聯接查詢的SQL語句:
select * from employss where first_name||''||last_name ='Beill Cliton';
上面這條語句徹底能夠查詢出是否有Bill Cliton這個員工,可是這裏須要注意,系統優化器對基於last_name建立的索引沒有使用。當採用下面這種SQL語句的編寫,Oracle系統就能夠採用基於last_name建立的索引。
*** where first_name ='Beill' and last_name ='Cliton';
(h) Order by語句
ORDER BY語句決定了Oracle如何將返回的查詢結果排序。Order by語句對要排序的列沒有什麼特別的限制,也能夠將函數加入列中(象聯接或者附加等)。任何在Order by語句的非索引項或者有計算表達式都將下降查詢速度。
仔細檢查order by語句以找出非索引項或者表達式,它們會下降性能。解決這個問題的辦法就是重寫order by語句以使用索引,也能夠爲所使用的列創建另一個索引,同時應絕對避免在order by子句中使用表達式。
(i) NOT
咱們在查詢時常常在where子句使用一些邏輯表達式,如大於、小於、等於以及不等於等等,也能夠使用and(與)、or(或)以及not(非)。NOT可用來對任何邏輯運算符號取反。下面是一個NOT子句的例子:
... where not (status ='VALID')
若是要使用NOT,則應在取反的短語前面加上括號,並在短語前面加上NOT運算符。NOT運算符包含在另一個邏輯運算符中,這就是不等於(<>)運算符。換句話說,即便不在查詢where子句中顯式地加入NOT詞,NOT仍在運算符中,見下例:
... where status <>'INVALID';
對這個查詢,能夠改寫爲不使用NOT:
select * from employee where salary<3000 or salary>3000;
雖然這兩種查詢的結果同樣,可是第二種查詢方案會比第一種查詢方案更快些。第二種查詢容許Oracle對salary列使用索引,而第一種查詢則不能使用索引。
2. SQL書寫的影響
(a) 同一功能同一性能不一樣寫法SQL的影響。
如一個SQL在A程序員寫的爲 Select * from zl_yhjbqk
B程序員寫的爲 Select * from dlyx.zl_yhjbqk(帶表全部者的前綴)
C程序員寫的爲 Select * from DLYX.ZLYHJBQK(大寫表名)
D程序員寫的爲 Select * from DLYX.ZLYHJBQK(中間多了空格)
以上四個SQL在ORACLE分析整理以後產生的結果及執行的時間是同樣的,可是從ORACLE共享內存SGA的原理,能夠得出ORACLE對每一個SQL 都會對其進行一次分析,而且佔用共享內存,若是將SQL的字符串及格式寫得徹底相同,則ORACLE只會分析一次,共享內存也只會留下一次的分析結果,這不只能夠減小分析SQL的時間,並且能夠減小共享內存重複的信息,ORACLE也能夠準確統計SQL的執行頻率。
(b) WHERE後面的條件順序影響
WHERE子句後面的條件順序對大數據量表的查詢會產生直接的影響。如:
Select * from zl_yhjbqk where dy_dj = '1KV如下' and xh_bz=1
Select * from zl_yhjbqk where xh_bz=1 and dy_dj = '1KV如下'
以上兩個SQL中dy_dj(電壓等級)及xh_bz(銷戶標誌)兩個字段都沒進行索引,因此執行的時候都是全表掃描,第一條SQL的dy_dj = '1KV如下'條件在記錄集內比率爲99%,而xh_bz=1的比率只爲0.5%,在進行第一條SQL的時候99%條記錄都進行dy_dj及xh_bz的比較,而在進行第二條SQL的時候0.5%條記錄都進行dy_dj及xh_bz的比較,以此能夠得出第二條SQL的CPU佔用率明顯比第一條低。
(c) 查詢表順序的影響
在FROM後面的表中的列表順序會對SQL執行性能影響,在沒有索引及ORACLE沒有對錶進行統計分析的狀況下,ORACLE會按表出現的順序進行連接,因而可知表的順序不對時會產生十分耗服物器資源的數據交叉。(注:若是對錶進行了統計分析,ORACLE會自動先進小表的連接,再進行大表的連接)
3. SQL語句索引的利用
(a) 對條件字段的一些優化
採用函數處理的字段不能利用索引,如:
substr(hbs_bh,1,4)=’5400’,優化處理:hbs_bh like ‘5400%’
trunc(sk_rq)=trunc(sysdate), 優化處理:sk_rq>=trunc(sysdate) and sk_rq<trunc(sysdate+1)
進行了顯式或隱式的運算的字段不能進行索引,如:ss_df+20>50,優化處理:ss_df>30
‘X’ || hbs_bh>’X5400021452’,優化處理:hbs_bh>’5400021542’
sk_rq+5=sysdate,優化處理:sk_rq=sysdate-5
hbs_bh=5401002554,優化處理:hbs_bh=’ 5401002554’,注:此條件對hbs_bh 進行隱式的to_number轉換,由於hbs_bh字段是字符型。
條件內包括了多個本表的字段運算時不能進行索引,如:
ys_df>cx_df,沒法進行優化
qc_bh || kh_bh=’5400250000’,優化處理:qc_bh=’5400’ and kh_bh=’250000’
4. 更多方面SQL優化資料分享
(1) 選擇最有效率的表名順序(只在基於規則的優化器中有效):
ORACLE 的解析器按照從右到左的順序處理FROM子句中的表名,FROM子句中寫在最後的表(基礎表 driving table)將被最早處理,在FROM子句中包含多個表的狀況下,你必須選擇記錄條數最少的表做爲基礎表。若是有3個以上的錶鏈接查詢, 那就須要選擇交叉表(intersection table)做爲基礎表, 交叉表是指那個被其餘表所引用的表.
(2) WHERE子句中的鏈接順序:
ORACLE採用自下而上的順序解析WHERE子句,根據這個原理,表之間的鏈接必須寫在其餘WHERE條件以前, 那些能夠過濾掉最大數量記錄的條件必須寫在WHERE子句的末尾.
(3) SELECT子句中避免使用 ‘ * ‘:
ORACLE在解析的過程當中, 會將'*' 依次轉換成全部的列名, 這個工做是經過查詢數據字典完成的, 這意味着將耗費更多的時間。
(4) 減小訪問數據庫的次數:
ORACLE在內部執行了許多工做: 解析SQL語句, 估算索引的利用率, 綁定變量 , 讀數據塊等。
(5) 在SQL*Plus , SQL*Forms和Pro*C中從新設置ARRAYSIZE參數, 能夠增長每次數據庫訪問的檢索數據量 ,建議值爲200。
(6) 使用DECODE函數來減小處理時間:
使用DECODE函數能夠避免重複掃描相同記錄或重複鏈接相同的表.
(7) 整合簡單,無關聯的數據庫訪問:
若是你有幾個簡單的數據庫查詢語句,你能夠把它們整合到一個查詢中(即便它們之間沒有關係) 。
(8) 刪除重複記錄:
最高效的刪除重複記錄方法 ( 由於使用了ROWID)例子:
DELETE FROM EMP E WHERE E.ROWID > (SELECT MIN(X.ROWID) FROM EMP X WHERE X.EMP_NO = E.EMP_NO)。
(9) 用TRUNCATE替代DELETE:
當刪除表中的記錄時,在一般狀況下, 回滾段(rollback segments ) 用來存放能夠被恢復的信息. 若是你沒有COMMIT事務,ORACLE會將數據恢復到刪除以前的狀態(準確地說是恢復到執行刪除命令以前的情況) 而當運用TRUNCATE時, 回滾段再也不存聽任何可被恢復的信息.當命令運行後,數據不能被恢復.所以不多的資源被調用,執行時間也會很短. (譯者按: TRUNCATE只在刪除全表適用,TRUNCATE是DDL不是DML) 。
(10) 儘可能多使用COMMIT:
只要有可能,在程序中儘可能多使用COMMIT, 這樣程序的性能獲得提升,需求也會由於COMMIT所釋放的資源而減小,COMMIT所釋放的資源:
a. 回滾段上用於恢復數據的信息.
b. 被程序語句得到的鎖
c. redo log buffer 中的空間
d. ORACLE爲管理上述3種資源中的內部花費
(11) 用Where子句替換HAVING子句:
避免使用HAVING子句, HAVING 只會在檢索出全部記錄以後纔對結果集進行過濾. 這個處理須要排序,總計等操做. 若是能經過WHERE子句限制記錄的數目,那就能減小這方面的開銷. (非oracle中)on、where、having這三個均可以加條件的子句中,on是最早執行,where次之,having最後,由於on是先把不符合條件的記錄過濾後才進行統計,它就能夠減小中間運算要處理的數據,按理說應該速度是最快的,where也應該比having快點的,由於它過濾數據後才進行sum,在兩個表聯接時才用on的,因此在一個表的時候,就剩下where跟having比較了。在這單表查詢統計的狀況下,若是要過濾的條件沒有涉及到要計算字段,那它們的結果是同樣的,只是where能夠使用rushmore技術,而having就不能,在速度上後者要慢若是要涉及到計算的字 段,就表示在沒計算以前,這個字段的值是不肯定的,根據上篇寫的工做流程,where的做用時間是在計算以前就完成的,而having就是在計算後才起做 用的,因此在這種狀況下,二者的結果會不一樣。在多表聯接查詢時,on比where更早起做用。系統首先根據各個表之間的聯接條件,把多個表合成一個臨時表 後,再由where進行過濾,而後再計算,計算完後再由having進行過濾。因而可知,要想過濾條件起到正確的做用,首先要明白這個條件應該在何時起做用,而後再決定放在那裏。
(12) 減小對錶的查詢:
在含有子查詢的SQL語句中,要特別注意減小對錶的查詢.例子:
SELECT TAB_NAME FROM TABLES WHERE (TAB_NAME,DB_VER) = ( SELECT TAB_NAME,DB_VER FROM TAB_COLUMNS WHERE VERSION = 604)
(13) 經過內部函數提升SQL效率:
複雜的SQL每每犧牲了執行效率. 可以掌握上面的運用函數解決問題的方法在實際工做中是很是有意義的。
(14) 使用表的別名(Alias):
當在SQL語句中鏈接多個表時, 請使用表的別名並把別名前綴於每一個Column上.這樣一來,就能夠減小解析的時間並減小那些由Column歧義引發的語法錯誤。
(15) 用EXISTS替代IN、用NOT EXISTS替代NOT IN:
在許多基於基礎表的查詢中,爲了知足一個條件,每每須要對另外一個表進行聯接.在這種狀況下, 使用EXISTS(或NOT EXISTS)一般將提升查詢的效率. 在子查詢中,NOT IN子句將執行一個內部的排序和合並. 不管在哪一種狀況下,NOT IN都是最低效的 (由於它對子查詢中的表執行了一個全表遍歷). 爲了不使用NOT IN ,咱們能夠把它改寫成外鏈接(Outer Joins)或NOT EXISTS。
例子:
(高效)SELECT * FROM EMP (基礎表) WHERE EMPNO > 0 AND EXISTS (SELECT ‘X' FROM DEPT WHERE DEPT.DEPTNO = EMP.DEPTNO AND LOC = ‘MELB')
(低效)SELECT * FROM EMP (基礎表) WHERE EMPNO > 0 AND DEPTNO IN(SELECT DEPTNO FROM DEPT WHERE LOC = ‘MELB')
(16) 識別'低效執行'的SQL語句:
雖然目前各類關於SQL優化的圖形化工具層出不窮,可是寫出本身的SQL工具來解決問題始終是一個最好的方法:
SELECT EXECUTIONS , DISK_READS, BUFFER_GETS,
ROUND((BUFFER_GETS-DISK_READS)/BUFFER_GETS,2) Hit_radio,
ROUND(DISK_READS/EXECUTIONS,2) Reads_per_run,
SQL_TEXT
FROM V$SQLAREA
WHERE EXECUTIONS>0
AND BUFFER_GETS > 0
AND (BUFFER_GETS-DISK_READS)/BUFFER_GETS < 0.8
ORDER BY 4 DESC;
(17) 用索引提升效率:
索引是表的一個概念部分,用來提升檢索數據的效率,ORACLE使用了一個複雜的自平衡B-tree結構. 一般,經過索引查詢數據比全表掃描要快. 當ORACLE找出執行查詢和Update語句的最佳路徑時, ORACLE優化器將使用索引. 一樣在聯結多個表時使用索引也能夠提升效率. 另外一個使用索引的好處是,它提供了主鍵(primary key)的惟一性驗證.。那些LONG或LONG RAW數據類型, 你能夠索引幾乎全部的列. 一般, 在大型表中使用索引特別有效. 固然,你也會發現, 在掃描小表時,使用索引一樣能提升效率. 雖然使用索引能獲得查詢效率的提升,可是咱們也必須注意到它的代價. 索引須要空間來存儲,也須要按期維護, 每當有記錄在表中增減或索引列被修改時, 索引自己也會被修改. 這意味着每條記錄的INSERT , DELETE , UPDATE將爲此多付出4 , 5 次的磁盤I/O . 由於索引須要額外的存儲空間和處理,那些沒必要要的索引反而會使查詢反應時間變慢.。按期的重構索引是有必要的:
ALTER INDEX <INDEXNAME> REBUILD <TABLESPACENAME>
(18) 用EXISTS替換DISTINCT:
當提交一個包含一對多表信息(好比部門表和僱員表)的查詢時,避免在SELECT子句中使用DISTINCT. 通常能夠考慮用EXIST替換, EXISTS 使查詢更爲迅速,由於RDBMS核心模塊將在子查詢的條件一旦知足後,馬上返回結果. 例子:
(低效):
SELECT DISTINCT DEPT_NO,DEPT_NAME FROM DEPT D , EMP E WHERE D.DEPT_NO = E.DEPT_NO
(高效):
SELECT DEPT_NO,DEPT_NAME FROM DEPT D WHERE EXISTS ( SELECT ‘X' FROM EMP E WHERE E.DEPT_NO = D.DEPT_NO);
(19) sql語句用大寫的;由於oracle老是先解析sql語句,把小寫的字母轉換成大寫的再執行。
(20) 在java代碼中儘可能少用鏈接符「+」鏈接字符串!
(21) 避免在索引列上使用NOT,一般咱們要避免在索引列上使用NOT, NOT會產生在和在索引列上使用函數相同的影響. 當ORACLE」遇到」NOT,他就會中止使用索引轉而執行全表掃描。
(22) 避免在索引列上使用計算
WHERE子句中,若是索引列是函數的一部分.優化器將不使用索引而使用全表掃描.舉例:
低效:
SELECT … FROM DEPT WHERE SAL * 12 > 25000;
高效:
SELECT … FROM DEPT WHERE SAL > 25000/12;
(23) 用>=替代>
高效:
SELECT * FROM EMP WHERE DEPTNO >=4
低效:
SELECT * FROM EMP WHERE DEPTNO >3
二者的區別在於, 前者DBMS將直接跳到第一個DEPT等於4的記錄然後者將首先定位到DEPTNO=3的記錄而且向前掃描到第一個DEPT大於3的記錄。
(24) 用UNION替換OR (適用於索引列)
一般狀況下, 用UNION替換WHERE子句中的OR將會起到較好的效果. 對索引列使用OR將形成全表掃描. 注意, 以上規則只針對多個索引列有效. 若是有column沒有被索引, 查詢效率可能會由於你沒有選擇OR而下降. 在下面的例子中, LOC_ID 和REGION上都建有索引.
高效:
SELECT LOC_ID , LOC_DESC , REGION
FROM LOCATION
WHERE LOC_ID = 10
UNION
SELECT LOC_ID , LOC_DESC , REGION
FROM LOCATION
WHERE REGION = 「MELBOURNE」
低效:
SELECT LOC_ID , LOC_DESC , REGION
FROM LOCATION
WHERE LOC_ID = 10 OR REGION = 「MELBOURNE」
若是你堅持要用OR, 那就須要返回記錄最少的索引列寫在最前面.
(25) 用IN來替換OR
這是一條簡單易記的規則,可是實際的執行效果還須檢驗,在ORACLE8i下,二者的執行路徑彷佛是相同的.
低效:
SELECT…. FROM LOCATION WHERE LOC_ID = 10 OR LOC_ID = 20 OR LOC_ID = 30
高效
SELECT… FROM LOCATION WHERE LOC_IN IN (10,20,30);
(26) 避免在索引列上使用IS NULL和IS NOT NULL
避免在索引中使用任何能夠爲空的列,ORACLE將沒法使用該索引.對於單列索引,若是列包含空值,索引中將不存在此記錄. 對於複合索引,若是每一個列都爲空,索引中一樣不存在此記錄. 若是至少有一個列不爲空,則記錄存在於索引中.舉例: 若是惟一性索引創建在表的A列和B列上, 而且表中存在一條記錄的A,B值爲(123,null) , ORACLE將不接受下一條具備相同A,B值(123,null)的記錄(插入). 然而若是全部的索引列都爲空,ORACLE將認爲整個鍵值爲空而空不等於空. 所以你能夠插入1000 條具備相同鍵值的記錄,固然它們都是空! 由於空值不存在於索引列中,因此WHERE子句中對索引列進行空值比較將使ORACLE停用該索引.
低效: (索引失效)
SELECT … FROM DEPARTMENT WHERE DEPT_CODE IS NOT NULL;
高效: (索引有效)
SELECT … FROM DEPARTMENT WHERE DEPT_CODE >=0;
(27) 老是使用索引的第一個列:
若是索引是創建在多個列上, 只有在它的第一個列(leading column)被where子句引用時,優化器纔會選擇使用該索引. 這也是一條簡單而重要的規則,當僅引用索引的第二個列時,優化器使用了全表掃描而忽略了索引。
(28) 用UNION-ALL 替換UNION ( 若是有可能的話):
當SQL 語句須要UNION兩個查詢結果集合時,這兩個結果集合會以UNION-ALL的方式被合併, 而後在輸出最終結果前進行排序. 若是用UNION ALL替代UNION, 這樣排序就不是必要了. 效率就會所以獲得提升. 須要注意的是,UNION ALL 將重複輸出兩個結果集合中相同記錄. 所以各位仍是要從業務需求分析使用UNION ALL的可行性. UNION 將對結果集合排序,這個操做會使用到SORT_AREA_SIZE這塊內存. 對於這塊內存的優化也是至關重要的. 下面的SQL能夠用來查詢排序的消耗量
低效:
SELECT ACCT_NUM, BALANCE_AMT
FROM DEBIT_TRANSACTIONS
WHERE TRAN_DATE = '31-DEC-95'
UNION
SELECT ACCT_NUM, BALANCE_AMT
FROM DEBIT_TRANSACTIONS
WHERE TRAN_DATE = '31-DEC-95'
高效:
SELECT ACCT_NUM, BALANCE_AMT
FROM DEBIT_TRANSACTIONS
WHERE TRAN_DATE = '31-DEC-95'
UNION ALL
SELECT ACCT_NUM, BALANCE_AMT
FROM DEBIT_TRANSACTIONS
WHERE TRAN_DATE = '31-DEC-95'
(29) 用WHERE替代ORDER BY:
ORDER BY 子句只在兩種嚴格的條件下使用索引.
ORDER BY中全部的列必須包含在相同的索引中並保持在索引中的排列順序.
ORDER BY中全部的列必須定義爲非空.
WHERE子句使用的索引和ORDER BY子句中所使用的索引不能並列.
例如:
表DEPT包含如下列:
DEPT_CODE PK NOT NULL
DEPT_DESC NOT NULL
DEPT_TYPE NULL
低效: (索引不被使用)
SELECT DEPT_CODE FROM DEPT ORDER BY DEPT_TYPE
高效: (使用索引)
SELECT DEPT_CODE FROM DEPT WHERE DEPT_TYPE > 0
(30) 避免改變索引列的類型:
當比較不一樣數據類型的數據時, ORACLE自動對列進行簡單的類型轉換.
假設 EMPNO是一個數值類型的索引列.
SELECT … FROM EMP WHERE EMPNO = ‘123'
實際上,通過ORACLE類型轉換, 語句轉化爲:
SELECT … FROM EMP WHERE EMPNO = TO_NUMBER(‘123')
幸運的是,類型轉換沒有發生在索引列上,索引的用途沒有被改變.
如今,假設EMP_TYPE是一個字符類型的索引列.
SELECT … FROM EMP WHERE EMP_TYPE = 123
這個語句被ORACLE轉換爲:
SELECT … FROM EMP WHERE TO_NUMBER(EMP_TYPE)=123
由於內部發生的類型轉換, 這個索引將不會被用到! 爲了不ORACLE對你的SQL進行隱式的類型轉換, 最好把類型轉換用顯式表現出來. 注意當字符和數值比較時, ORACLE會優先轉換數值類型到字符類型。
分析select emp_name form employee where salary > 3000 在此語句中若salary是Float類型的,則優化器對其進行優化爲Convert(float,3000),由於3000是個整數,咱們應在編程時使用3000.0而不要等運行時讓DBMS進行轉化。一樣字符和整型數據的轉換。
(31) 須要小心的WHERE子句:
某些SELECT 語句中的WHERE子句不使用索引. 這裏有一些例子.
在下面的例子裏, (1)‘!=' 將不使用索引. 記住, 索引只能告訴你什麼存在於表中, 而不能告訴你什麼不存在於表中. (2) ‘ ¦ ¦'是字符鏈接函數. 就象其餘函數那樣, 停用了索引. (3) ‘+'是數學函數. 就象其餘數學函數那樣, 停用了索引. (4)相同的索引列不能互相比較,這將會啓用全表掃描.
(32) a. 若是檢索數據量超過30%的表中記錄數.使用索引將沒有顯著的效率提升. b. 在特定狀況下, 使用索引也許會比全表掃描慢, 但這是同一個數量級上的區別. 而一般狀況下,使用索引比全表掃描要塊幾倍乃至幾千倍!
(33) 避免使用耗費資源的操做:
帶有DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的SQL語句會啓動SQL引擎執行耗費資源的排序(SORT)功能. DISTINCT須要一次排序操做, 而其餘的至少須要執行兩次排序. 一般, 帶有UNION, MINUS , INTERSECT的SQL語句均可以用其餘方式重寫. 若是你的數據庫的SORT_AREA_SIZE調配得好, 使用UNION , MINUS, INTERSECT也是能夠考慮的, 畢竟它們的可讀性很強。
(34) 優化GROUP BY:
提升GROUP BY 語句的效率, 能夠經過將不須要的記錄在GROUP BY 以前過濾掉.下面兩個查詢返回相同結果但第二個明顯就快了許多. 低效: SELECT JOB , AVG(SAL) FROM EMP GROUP by JOB HAVING JOB = ‘PRESIDENT' OR JOB = ‘MANAGER' 高效: SELECT JOB , AVG(SAL) FROM EMP WHERE JOB = ‘PRESIDENT' OR JOB = ‘MANAGER' GROUP by JOB