轉載位址:https://blog.csdn.net/yzllz001/article/details/54848513
1.資料庫通路優化法則
要正确的優化SQL,我們需要快速定位能性的瓶頸點,也就是說快速找到我們SQL主要的開銷在哪裡?而大多數情況性能最慢的裝置會是瓶頸點,如下載下傳時網絡速度可能會是瓶頸點,本地複制檔案時硬碟可能會是瓶頸點,為什麼這些一般的工作我們能快速确認瓶頸點呢,因為我們對這些慢速裝置的性能資料有一些基本的認識,如網絡帶寬是2Mbps,硬碟是每分鐘7200轉等等。是以,為了快速找到SQL的性能瓶頸點,我們也需要了解我們計算機系統的硬體基本性能名額,下圖展示的目前主流計算機性能名額資料。

從圖上可以看到基本上每種裝置都有兩個名額:
延時(響應時間):表示硬體的突發處理能力;
帶寬(吞吐量):代表硬體持續處理能力。
從上圖可以看出,計算機系統硬體性能從高到代依次為:
CPU——Cache(L1-L2-L3)——記憶體——SSD硬碟——網絡——硬碟
由于SSD硬碟還處于快速發展階段,是以本文的内容不涉及SSD相關應用系統。
根據資料庫知識,我們可以列出每種硬體主要的工作内容:
CPU及記憶體:緩存資料通路、比較、排序、事務檢測、SQL解析、函數或邏輯運算;
網絡:結果資料傳輸、SQL請求、遠端資料庫通路(dblink);
硬碟:資料通路、資料寫入、日志記錄、大資料量排序、大表連接配接。
根據目前計算機硬體的基本性能名額及其在資料庫中主要操作内容,可以整理出如下圖所示的性能基本優化法則:
這個優化法則歸納為5個層次:
1、 減少資料通路(減少磁盤通路)
2、 傳回更少資料(減少網絡傳輸或磁盤通路)
3、 減少互動次數(減少網絡傳輸)
4、 減少伺服器CPU開銷(減少CPU及記憶體開銷)
5、 利用更多資源(增加資源)
由于每一層優化法則都是解決其對應硬體的性能問題,是以帶來的性能提升比例也不一樣。傳統資料庫系統設計是也是盡可能對低速裝置提供優化方法,是以針對低速裝置問題的可優化手段也更多,優化成本也更低。我們任何一個SQL的性能優化都應該按這個規則由上到下來診斷問題并提出解決方案,而不應該首先想到的是增加資源解決問題。
以下是每個優化法則層級對應優化效果及成本經驗參考:
優化法則 | 性能提升效果 | 優化成本 |
減少資料通路 | 1~1000 | 低 |
傳回更少資料 | 1~100 | 低 |
減少互動次數 | 1~20 | 低 |
減少伺服器CPU開銷 | 1~5 | 低 |
利用更多資源 | @~10 | 高 |
接下來,我們針對5種優化法則列舉常用的優化手段并結合執行個體分析。
二、Oracle資料庫兩個基本概念
資料塊(Block)
資料塊是資料庫中資料在磁盤中存儲的最小機關,也是一次IO通路的最小機關,一個資料塊通常可以存儲多條記錄,資料塊大小是DBA在建立資料庫或表空間時指定,可指定為2K、4K、8K、16K或32K位元組。下圖是一個Oracle資料庫典型的實體結構,一個資料庫可以包括多個資料檔案,一個資料檔案内又包含多個資料塊;
ROWID
ROWID是每條記錄在資料庫中的唯一辨別,通過ROWID可以直接定位記錄到對應的檔案号及資料塊位置。ROWID内容包括檔案号、對像号、資料塊号、記錄槽号,如下圖所示:
三、資料庫通路優化法則詳解
1、減少資料通路
1.1、建立并使用正确的索引
資料庫索引的原理非常簡單,但在複雜的表中真正能正确使用索引的人很少,即使是專業的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提高性能的條件比較特殊,使用不好反而性能會更差。 |
我們一般在什麼字段上建索引?
這是一個非常複雜的話題,需要對業務及資料充分分析後再能得出結果。主鍵及外鍵通常都要有索引,其它需要建索引的字段應滿足以下條件:
1、字段出現在查詢條件中,并且查詢條件可以使用索引;
2、語句執行頻率高,一天會有幾千次以上;
3、通過字段條件可篩選的記錄集很小,那資料篩選比例是多少才适合?
這個沒有固定值,需要根據表資料量來評估,以下是經驗公式,可用于快速評估:
小表(記錄數小于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壓力比較大的系統,表的索引需要仔細評估必要性,另外索引也會占用一定的存儲空間。
1.2、隻通過索引通路資料
有些時候,我們隻是通路表中的幾個字段,并且字段内容較少,我們可以為這幾個字段單獨建立一個組合索引,這樣就可以直接隻通過通路索引就能得到資料,一般索引占用的磁盤空間比表小很多,是以這種方式可以大大減少磁盤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請求的字段都建在索引裡,是以這種隻通過索引通路資料的方法一般隻用于核心應用,也就是那種對核心表通路量最高且查詢字段資料量很少的查詢。
1.3、優化SQL執行計劃
SQL執行計劃是關系型資料庫最核心的技術之一,它表示SQL執行時的資料通路算法。由于業務需求越來越複雜,表資料量也越來越大,程式員越來越懶惰,SQL也需要支援非常複雜的業務邏輯,但SQL的性能還需要提高,是以,優秀的關系型資料庫除了需要支援複雜的SQL文法及更多函數外,還需要有一套優秀的算法庫來提高SQL性能。
目前ORACLE有SQL執行計劃的算法約300種,而且一直在增加,是以SQL執行計劃是一個非常複雜的課題,一個普通DBA能掌握50種就很不錯了,就算是資深DBA也不可能把每個執行計劃的算法描述清楚。雖然有這麼多種算法,但并不表示我們無法優化執行計劃,因為我們常用的SQL執行計劃算法也就十幾個,如果一個程式員能把這十幾個算法搞清楚,那就掌握了80%的SQL執行計劃調優知識。
由于篇幅的原因,SQL執行計劃需要專題介紹,在這裡就不多說了。
2、傳回更少的資料
2.1、資料分頁處理
一般資料分頁方式有:
2.1.1、用戶端(應用程式或浏覽器)分頁
将資料從應用伺服器全部下載下傳到本地應用程式或浏覽器,在應用程式或浏覽器内部通過本地代碼進行分頁處理
優點:編碼簡單,減少用戶端與應用伺服器網絡互動次數
缺點:首次互動時間長,占用用戶端記憶體
适應場景:用戶端與應用伺服器網絡延時較大,但要求後續操作流暢,如手機GPRS,超遠端通路(跨國)等等。
2.1.2、應用伺服器分頁
将資料從資料庫伺服器全部下載下傳到應用伺服器,在應用伺服器内部再進行資料篩選。以下是一個應用伺服器端Java程式分頁的示例:
List list=executeQuery(“select * from employee order by id”);
Int count= list.size();
List subList= list.subList(10, 20);
優點:編碼簡單,隻需要一次SQL互動,總資料與分頁資料差不多時性能較好。
缺點:總資料量較多時性能較差。
适應場景:資料庫系統不支援分頁處理,資料量較小并且可控。
2.1.3、資料庫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);
2.2、隻傳回需要的字段
通過去除不必要的傳回字段可以提高性能,例:
調整前:select * from product where company_id=?;
調整後:select id,name from product where company_id=?;
優點:
1、減少資料在網絡上傳輸開銷
2、減少伺服器資料處理開銷
3、減少用戶端記憶體占用
4、字段變更時提前發現問題,減少程式BUG
5、如果通路的所有字段剛好在一個索引裡面,則可以使用純索引通路提高性能。
缺點:增加編碼工作量
由于會增加一些編碼工作量,是以一般需求通過開發規範來要求程式員這麼做,否則等項目上線後再整改工作量更大。
如果你的查詢表中有大字段或内容較多的字段,如備注資訊、檔案内容等等,那在查詢表時一定要注意這方面的問題,否則可能會帶來嚴重的性能問題。如果表經常要查詢并且請求大内容字段的機率很低,我們可以采用分表處理,将一個大表分拆成兩個一對一的關系表,将不常用的大内容字段放在一張單獨的表中。如一張存儲上傳檔案的表:
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表。
3、減少互動次數
3.1、batch DML
資料庫通路架構一般都提供了批量送出的接口,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通路。以上僅是理論計算值,實際情況需要根據具體環境測量。
3.2、In List
很多時候我們需要按一些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評估。
3.3、設定Fetch Size
當我們采用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>
3.4、使用存儲過程
大型資料庫一般都支援存儲過程,合理的利用存儲過程也可以提高系統性能。如你有一個業務需要将A表的資料做一些加工然後更新到B表中,但是又不可能一條SQL完成,這時你需要如下3步操作:
a:将A表資料全部取出到用戶端;
b:計算出要更新的資料;
c:将計算結果更新到B表。
如果采用存儲過程你可以将整個業務邏輯封裝在存儲過程裡,然後在用戶端直接調用存儲過程處理,這樣可以減少網絡互動的成本。
當然,存儲過程也并不是十全十美,存儲過程有以下缺點:
a、不可移植性,每種資料庫的内部程式設計文法都不太相同,當你的系統需要相容多種資料庫時最好不要用存儲過程。
b、學習成本高,DBA一般都擅長寫存儲過程,但并不是每個程式員都能寫好存儲過程,除非你的團隊有較多的開發人員熟悉寫存儲過程,否則後期系統維護會産生問題。
c、業務邏輯多處存在,采用存儲過程後也就意味着你的系統有一些業務邏輯不是在應用程式裡處理,這種架構會增加一些系統維護和調試成本。
d、存儲過程和常用應用程式語言不一樣,它支援的函數及文法有可能不能滿足需求,有些邏輯就隻能通過應用程式處理。
e、如果存儲過程中有複雜運算的話,會增加一些資料庫服務端的處理成本,對于集中式資料庫可能會導緻系統可擴充性問題。
f、為了提高性能,資料庫會把存儲過程代碼編譯成中間運作代碼(類似于java的class檔案),是以更像靜态語言。當存儲過程引用的對像(表、視圖等等)結構改變後,存儲過程需要重新編譯才能生效,在24*7高并發應用場景,一般都是線上變更結構的,是以在變更的瞬間要同時編譯存儲過程,這可能會導緻資料庫瞬間壓力上升引起故障(Oracle資料庫就存在這樣的問題)。
個人觀點:普通業務邏輯盡量不要使用存儲過程,定時性的ETL任務或報表統計函數可以根據團隊資源情況采用存儲過程處理。
3.5、優化業務邏輯
要通過優化業務邏輯來提高性能是比較困難的,這需要程式員對所通路的資料及業務流程非常清楚。
舉一個案例:
某移動公司推出優惠套參,活動對像為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的互動次數。
以上隻是一個簡單的示例,實際的業務總是比這複雜得多,是以一般隻是進階程式員更容易做出優化的邏輯,但是我們需要有這樣一種成本優化的意識。
3.6、使用ResultSet遊标處理記錄
現在大部分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記憶體溢出。
4、減少資料庫伺服器CPU運算
4.1、使用綁定變量
綁定變量是指SQL中對變化的值采用變量參數的形式送出,而不是在SQL中直接拼寫對應的值。
非綁定變量寫法:Select * from employee where id=1234567
綁定變量寫法:
Select * from employee where id=?
Preparestatement.setInt(1,1234567)
Java中Preparestatement就是為處理綁定變量提供的對像,綁定變量有以下優點:
1、防止SQL注入
2、提高SQL可讀性
3、提高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';//使用索引
4.2、合理使用排序
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執行計劃優化專題。
4.3、減少比較操作
我們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溝通并确認準确的原因。
4.4、大量複雜運算在用戶端處理
什麼是複雜運算,一般我認為是一秒鐘CPU隻能做10萬次以内的運算。如含小數的對數及指數運算、三角函數、3DES及BASE64資料加密算法等等。
如果有大量這類函數運算,盡量放在用戶端處理,一般CPU每秒中也隻能處理1萬-10萬次這樣的函數運算,放在資料庫内不利于高并發處理。
5、利用更多的資源
5.1、用戶端多程序并行通路
多程序并行通路是指在用戶端建立多個程序(線程),每個程序建立一個與資料庫的連接配接,然後同時向資料庫送出通路請求。當資料庫主機資源有空閑時,我們可以采用用戶端多程序并行通路的方法來提高性能。如果資料庫主機已經很忙時,采用多程序并行通路性能不會提高,反而可能會更慢。是以使用這種方式最好與DBA或系統管理者進行溝通後再決定是否采用。
例如:
我們有10000個産品ID,現在需要根據ID取出産品的詳細資訊,如果單線程通路,按每個IO要5ms計算,忽略主機CPU運算及網絡傳輸時間,我們需要50s才能完成任務。如果采用5個并行通路,每個程序通路2000個ID,那麼10s就有可能完成任務。
那是不是并行數越多越好呢,開1000個并行是否隻要50ms就搞定,答案肯定是否定的,當并行數超過伺服器主機資源的上限時性能就不會再提高,如果再增加反而會增加主機的程序間排程成本和程序沖突機率。
以下是一些如何設定并行數的基本建議:
如果瓶頸在伺服器主機,但是主機還有空閑資源,那麼最大并行數取主機CPU核數和主機提供資料服務的磁盤數兩個參數中的最小值,同時要保證主機有資源做其它任務。
如果瓶頸在用戶端處理,但是用戶端還有空閑資源,那建議不要增加SQL的并行,而是用一個程序取回資料後在用戶端起多個程序處理即可,程序數根據用戶端CPU核數計算。
如果瓶頸在用戶端網絡,那建議做資料壓縮或者增加多個用戶端,采用map reduce的架構處理。
如果瓶頸在伺服器網絡,那需要增加伺服器的網絡帶寬或者在服務端将資料壓縮後再處理了。
5.2、資料庫并行處理
資料庫并行處理是指用戶端一條SQL的請求,資料庫内部自動分解成多個程序并行處理,如下圖所示:
并不是所有的SQL都可以使用并行處理,一般隻有對表或索引進行全部通路時才可以使用并行。資料庫表預設是不打開并行通路,是以需要指定SQL并行的提示,如下所示:
select * from employee;
并行的優點:
使用多程序處理,充分利用資料庫主機資源(CPU,IO),提高性能。
并行的缺點:
1、單個會話占用大量資源,影響其它會話,是以隻适合在主機負載低時期使用;
2、隻能采用直接IO通路,不能利用緩存資料,是以執行前會觸發将髒緩存資料寫入磁盤操作。
注:
1、并行處理在OLTP類系統中慎用,使用不當會導緻一個會話把主機資源全部占用,而正常事務得不到及時響應,是以一般隻是用于資料倉庫平台。
2、一般對于百萬級記錄以下的小表采用并行通路性能并不能提高,反而可能會讓性能更差。
SQL性能優化 --- 面試題
今天面試,我履歷上寫了熟悉sql的性能優化,但是今天面試,一時想不起别的,就僅僅說出了一條,在這裡再總結一些,完善自己的知識點。
我經常用的資料庫是oracle,是以我的sql優化是程式員針對于oracle的。
總結,這個sql優化是針對程式員的,而不是針對dba的,主要就是第一,盡量防止模糊,明确指出,即用列名代替*,第二,在where語句上下工夫。第三多表查詢和子查詢,第四盡量使用綁定。
資料庫性能優化之SQL語句優化1
一、問題的提出
在應用系統開發初期,由于開發資料庫資料比較少,對于查詢SQL語句,複雜視圖的的編寫等體會不出SQL語句各種寫法的性能優劣,但是如果将應用系統送出實際應用後,随着資料庫中資料的增加,系統的響應速度就成為目前系統需要解決的最主要的問題之一。系統優化中一個很重要的方面就是SQL語句的優化。對于海量資料,劣質SQL語句和優質SQL語句之間的速度差别可以達到上百倍,可見對于一個系統不是簡單地能實作其功能就可,而是要寫出高品質的SQL語句,提高系統的可用性。
在多數情況下,Oracle使用索引來更快地周遊表,優化器主要根據定義的索引來提高性能。但是,如果在SQL語句的where子句中寫的SQL代碼不合理,就會造成優化器删去索引而使用全表掃描,一般就這種SQL語句就是所謂的劣質SQL語句。在編寫SQL語句時我們應清楚優化器根據何種原則來删除索引,這有助于寫出高性能的SQL語句。
二、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
轉自:http://my.oschina.net/xianggao/blog/87216
另有參考 http://my.oschina.net/xianggao/blog/87448 資料庫性能優化之SQL語句優化2
http://my.oschina.net/xianggao/blog/87450 資料庫性能優化之SQL語句優化3
http://my.oschina.net/xianggao/blog/87453 資料庫性能優化之SQL語句優化4
http://my.oschina.net/xianggao/blog/87223 關于如何形成一個好的資料庫設計
SQL性能優化二
- 優化目标
-
減少 IO 次數
IO永遠是資料庫最容易瓶頸的地方,這是由資料庫的職責所決定的,大部分資料庫操作中超過90%的時間都是 IO 操作所占用的,減少 IO 次數是 SQL 優化中需要第一優先考慮,當然,也是收效最明顯的優化手段。
-
降低 CPU 計算
除了 IO 瓶頸之外,SQL優化中需要考慮的就是 CPU 運算量的優化了。order by, group by,distinct … 都是消耗 CPU 的大戶(這些操作基本上都是 CPU 處理記憶體中的資料比較運算)。當我們的 IO 優化做到一定階段之後,降低 CPU 計算也就成為了我們 SQL 優化的重要目标
-
- 優化方法
-
改變 SQL 執行計劃
明确了優化目标之後,我們需要确定達到我們目标的方法。對于 SQL 語句來說,達到上述2個目标的方法其實隻有一個,那就是改變 SQL 的執行計劃,讓他盡量“少走彎路”,盡量通過各種“捷徑”來找到我們需要的資料,以達到 “減少 IO 次數” 和 “降低 CPU 計算” 的目标
-
- 常見誤區
-
count(1)和count(primary_key) 優于 count(*)
很多人為了統計記錄條數,就使用 count(1) 和 count(primary_key) 而不是 count(*) ,他們認為這樣性能更好,其實這是一個誤區。對于有些場景,這樣做可能性能會更差,應為資料庫對 count(*) 計數操作做了一些特别的優化。
-
count(column) 和 count(*) 是一樣的
這個誤區甚至在很多的資深工程師或者是 DBA 中都普遍存在,很多人都會認為這是理所當然的。實際上,count(column) 和 count(*) 是一個完全不一樣的操作,所代表的意義也完全不一樣。
count(column) 是表示結果集中有多少個column字段不為空的記錄
count(*) 是表示整個結果集有多少條記錄
-
select a,b from … 比 select a,b,c from … 可以讓資料庫通路更少的資料量
這個誤區主要存在于大量的開發人員中,主要原因是對資料庫的存儲原理不是太了解。
實際上,大多數關系型資料庫都是按照行(row)的方式存儲,而資料存取操作都是以一個固定大小的IO單元(被稱作 block 或者 page)為機關,一般為4KB,8KB… 大多數時候,每個IO單元中存儲了多行,每行都是存儲了該行的所有字段(lob等特殊類型字段除外)。
是以,我們是取一個字段還是多個字段,實際上資料庫在表中需要通路的資料量其實是一樣的。
當然,也有例外情況,那就是我們的這個查詢在索引中就可以完成,也就是說當隻取 a,b兩個字段的時候,不需要回表,而c這個字段不在使用的索引中,需要回表取得其資料。在這樣的情況下,二者的IO量會有較大差異。
-
order by 一定需要排序操作
我們知道索引資料實際上是有序的,如果我們的需要的資料和某個索引的順序一緻,而且我們的查詢又通過這個索引來執行,那麼資料庫一般會省略排序操作,而直接将資料傳回,因為資料庫知道資料已經滿足我們的排序需求了。
實際上,利用索引來優化有排序需求的 SQL,是一個非常重要的優化手段
延伸閱讀:MySQL ORDER BY 的實作分析 ,MySQL 中 GROUP BY 基本實作原理 以及 MySQL DISTINCT 的基本實作原理 這3篇文章中有更為深入的分析,尤其是第一篇
-
執行計劃中有 filesort 就會進行磁盤檔案排序
有這個誤區其實并不能怪我們,而是因為 MySQL 開發者在用詞方面的問題。filesort 是我們在使用 explain 指令檢視一條 SQL 的執行計劃的時候可能會看到在 “Extra” 一列顯示的資訊。
實際上,隻要一條 SQL 語句需要進行排序操作,都會顯示“Using filesort”,這并不表示就會有檔案排序操作。
延伸閱讀:了解 MySQL Explain 指令輸出中的filesort,我在這裡有更為詳細的介紹
-
- 基本原則
-
盡量少 join
MySQL 的優勢在于簡單,但這在某些方面其實也是其劣勢。MySQL 優化器效率高,但是由于其統計資訊的量有限,優化器工作過程出現偏差的可能性也就更多。對于複雜的多表 Join,一方面由于其優化器受限,再者在 Join 這方面所下的功夫還不夠,是以性能表現離 Oracle 等關系型資料庫前輩還是有一定距離。但如果是簡單的單表查詢,這一差距就會極小甚至在有些場景下要優于這些資料庫前輩。
-
盡量少排序
排序操作會消耗較多的 CPU 資源,是以減少排序可以在緩存命中率高等 IO 能力足夠的場景下會較大影響 SQL 的響應時間。
對于MySQL來說,減少排序有多種辦法,比如:
- 上面誤區中提到的通過利用索引來排序的方式進行優化
- 減少參與排序的記錄條數
- 非必要不對資料進行排序
- …
-
盡量避免 select *
很多人看到這一點後覺得比較難了解,上面不是在誤區中剛剛說 select 子句中字段的多少并不會影響到讀取的資料嗎?
是的,大多數時候并不會影響到 IO 量,但是當我們還存在 order by 操作的時候,select 子句中的字段多少會在很大程度上影響到我們的排序效率,這一點可以通過我之前一篇介紹 MySQL ORDER BY 的實作分析 的文章中有較為詳細的介紹。
此外,上面誤區中不是也說了,隻是大多數時候是不會影響到 IO 量,當我們的查詢結果僅僅隻需要在索引中就能找到的時候,還是會極大減少 IO 量的。
-
盡量用 join 代替子查詢
雖然 Join 性能并不佳,但是和 MySQL 的子查詢比起來還是有非常大的性能優勢。MySQL 的子查詢執行計劃一直存在較大的問題,雖然這個問題已經存在多年,但是到目前已經釋出的所有穩定版本中都普遍存在,一直沒有太大改善。雖然官方也在很早就承認這一問題,并且承諾盡快解決,但是至少到目前為止我們還沒有看到哪一個版本較好的解決了這一問題。
-
盡量少 or
當 where 子句中存在多個條件以“或”并存的時候,MySQL 的優化器并沒有很好的解決其執行計劃優化問題,再加上 MySQL 特有的 SQL 與 Storage 分層架構方式,造成了其性能比較低下,很多時候使用 union all 或者是union(必要的時候)的方式來代替“or”會得到更好的效果。
-
盡量用 union all 代替 union
union 和 union all 的差異主要是前者需要将兩個(或者多個)結果集合并後再進行唯一性過濾操作,這就會涉及到排序,增加大量的 CPU 運算,加大資源消耗及延遲。是以當我們可以确認不可能出現重複結果集或者不在乎重複結果集的時候,盡量使用 union all 而不是 union。
-
盡量早過濾
這一優化政策其實最常見于索引的優化設計中(将過濾性更好的字段放得更靠前)。
在 SQL 編寫中同樣可以使用這一原則來優化一些 Join 的 SQL。比如我們在多個表進行分頁資料查詢的時候,我們最好是能夠在一個表上先過濾好資料分好頁,然後再用分好頁的結果集與另外的表 Join,這樣可以盡可能多的減少不必要的 IO 操作,大大節省 IO 操作所消耗的時間。
-
避免類型轉換
這裡所說的“類型轉換”是指 where 子句中出現 column 字段的類型和傳入的參數類型不一緻的時候發生的類型轉換:
-
人為在column_name 上通過轉換函數進行轉換
直接導緻 MySQL(實際上其他資料庫也會有同樣的問題)無法使用索引,如果非要轉換,應該在傳入的參數上進行轉換
-
由資料庫自己進行轉換
如果我們傳入的資料類型和字段類型不一緻,同時我們又沒有做任何類型轉換處理,MySQL 可能會自己對我們的資料進行類型轉換操作,也可能不進行處理而交由存儲引擎去處理,這樣一來,就會出現索引無法使用的情況而造成執行計劃問題。
-
-
優先優化高并發的 SQL,而不是執行頻率低某些“大”SQL
對于破壞性來說,高并發的 SQL 總是會比低頻率的來得大,因為高并發的 SQL 一旦出現問題,甚至不會給我們任何喘息的機會就會将系統壓跨。而對于一些雖然需要消耗大量 IO 而且響應很慢的 SQL,由于頻率低,即使遇到,最多就是讓整個系統響應慢一點,但至少可能撐一會兒,讓我們有緩沖的機會。
-
從全局出發優化,而不是片面調整
SQL 優化不能是單獨針對某一個進行,而應充分考慮系統中所有的 SQL,尤其是在通過調整索引優化 SQL 的執行計劃的時候,千萬不能顧此失彼,因小失大。
-
盡可能對每一條運作在資料庫中的SQL進行 explain
優化 SQL,需要做到心中有數,知道 SQL 的執行計劃才能判斷是否有優化餘地,才能判斷是否存在執行計劃問題。在對資料庫中運作的 SQL 進行了一段時間的優化之後,很明顯的問題 SQL 可能已經很少了,大多都需要去發掘,這時候就需要進行大量的 explain 操作收集執行計劃,并判斷是否需要進行優化
Mysql性能優化之引擎的選擇
MySQL 的存儲引擎可能是所有關系型資料庫産品中最具有特色的了,不僅可以同時使用多種存儲引擎,而且每種存儲引擎和MySQL之間使用插件方式這種非常松的耦合關系。
由于各存儲引擎功能特性差異較大,這篇文章主要是介紹如何來選擇合适的存儲引擎來應對不同的業務場景。
- MyISAM
- 特性
- 不支援事務:MyISAM存儲引擎不支援事務,是以對事務有要求的業務場景不能使用
- 表級鎖定:其鎖定機制是表級索引,這雖然可以讓鎖定的實作成本很小但是也同時大大降低了其并發性能
- 讀寫互相阻塞:不僅會在寫入的時候阻塞讀取,MyISAM還會在讀取的時候阻塞寫入,但讀本身并不會阻塞另外的讀
- 隻會緩存索引:MyISAM可以通過key_buffer緩存以大大提高通路性能減少磁盤IO,但是這個緩存區隻會緩存索引,而不會緩存資料
- 适用場景
- 不需要事務支援(不支援)
- 并發相對較低(鎖定機制問題)
- 資料修改相對較少(阻塞問題)
- 以讀為主
- 資料一緻性要求不是非常高
- 最佳實踐
- 盡量索引(緩存機制)
- 調整讀寫優先級,根據實際需求確定重要操作更優先
- 啟用延遲插入改善大批量寫入性能
- 盡量順序操作讓insert資料都寫入到尾部,減少阻塞
- 分解大的操作,降低單個操作的阻塞時間
- 降低并發數,某些高并發場景通過應用來進行排隊機制
- 對于相對靜态的資料,充分利用Query Cache可以極大的提高通路效率
- MyISAM的Count隻有在全表掃描的時候特别高效,帶有其他條件的count都需要進行實際的資料通路
- 特性
- InnoDB
- 特性
- 具有較好的事務支援:支援4個事務隔離級别,支援多版本讀
- 行級鎖定:通過索引實作,全表掃描仍然會是表鎖,注意間隙鎖的影響
- 讀寫阻塞與事務隔離級别相關
- 具有非常高效的緩存特性:能緩存索引,也能緩存資料
- 整個表和主鍵以Cluster方式存儲,組成一顆平衡樹
- 所有Secondary Index都會儲存主鍵資訊
- 适用場景
- 需要事務支援(具有較好的事務特性)
- 行級鎖定對高并發有很好的适應能力,但需要確定查詢是通過索引完成
- 資料更新較為頻繁的場景
- 資料一緻性要求較高
- 硬體裝置記憶體較大,可以利用InnoDB較好的緩存能力來提高記憶體使用率,盡可能減少磁盤 IO
- 最佳實踐
- 主鍵盡可能小,避免給Secondary index帶來過大的空間負擔
- 避免全表掃描,因為會使用表鎖
- 盡可能緩存所有的索引和資料,提高響應速度
- 在大批量小插入的時候,盡量自己控制事務而不要使用autocommit自動送出
- 合理設定innodb_flush_log_at_trx_commit參數值,不要過度追求安全性
- 避免主鍵更新,因為這會帶來大量的資料移動
- 特性
- NDBCluster
- 特性
- 分布式:分布式存儲引擎,可以由多個NDBCluster存儲引擎組成叢集分别存放整體資料的一部分
- 支援事務:和Innodb一樣,支援事務
- 可與mysqld不在一台主機:可以和mysqld分開存在于獨立的主機上,然後通過網絡和mysqld通信互動
- 記憶體需求量巨大:新版本索引以及被索引的資料必須存放在記憶體中,老版本所有資料和索引必須存在與記憶體中
- 适用場景
- 具有非常高的并發需求
- 對單個請求的響應并不是非常的critical
- 查詢簡單,過濾條件較為固定,每次請求資料量較少,又不希望自己進行水準Sharding
- 最佳實踐
- 盡可能讓查詢簡單,避免資料的跨節點傳輸
- 盡可能滿足SQL節點的計算性能,大一點的叢集SQL節點會明顯多餘Data節點
- 在各節點之間盡可能使用萬兆網絡環境互聯,以減少資料在網絡層傳輸過程中的延時
- 特性
Mysql性能優化 --- 包含SQL、表結構、索引和緩存
- 優化目标
-
減少 IO 次數
IO永遠是資料庫最容易瓶頸的地方,這是由資料庫的職責所決定的,大部分資料庫操作中超過90%的時間都是 IO 操作所占用的,減少 IO 次數是 SQL 優化中需要第一優先考慮,當然,也是收效最明顯的優化手段。
-
降低 CPU 計算
除了 IO 瓶頸之外,SQL優化中需要考慮的就是 CPU 運算量的優化了。order by, group by,distinct … 都是消耗 CPU 的大戶(這些操作基本上都是 CPU 處理記憶體中的資料比較運算)。當我們的 IO 優化做到一定階段之後,降低 CPU 計算也就成為了我們 SQL 優化的重要目标
-
- 優化方法
-
改變 SQL 執行計劃
明确了優化目标之後,我們需要确定達到我們目标的方法。對于 SQL 語句來說,達到上述2個目标的方法其實隻有一個,那就是改變 SQL 的執行計劃,讓他盡量“少走彎路”,盡量通過各種“捷徑”來找到我們需要的資料,以達到 “減少 IO 次數” 和 “降低 CPU 計算” 的目标
-
- 常見誤區
-
count(1)和count(primary_key) 優于 count(*)
很多人為了統計記錄條數,就使用 count(1) 和 count(primary_key) 而不是 count(*) ,他們認為這樣性能更好,其實這是一個誤區。對于有些場景,這樣做可能性能會更差,應為資料庫對 count(*) 計數操作做了一些特别的優化。
-
count(column) 和 count(*) 是一樣的
這個誤區甚至在很多的資深工程師或者是 DBA 中都普遍存在,很多人都會認為這是理所當然的。實際上,count(column) 和 count(*) 是一個完全不一樣的操作,所代表的意義也完全不一樣。
count(column) 是表示結果集中有多少個column字段不為空的記錄
count(*) 是表示整個結果集有多少條記錄
-
select a,b from … 比 select a,b,c from … 可以讓資料庫通路更少的資料量
這個誤區主要存在于大量的開發人員中,主要原因是對資料庫的存儲原理不是太了解。
實際上,大多數關系型資料庫都是按照行(row)的方式存儲,而資料存取操作都是以一個固定大小的IO單元(被稱作 block 或者 page)為機關,一般為4KB,8KB… 大多數時候,每個IO單元中存儲了多行,每行都是存儲了該行的所有字段(lob等特殊類型字段除外)。
是以,我們是取一個字段還是多個字段,實際上資料庫在表中需要通路的資料量其實是一樣的。
當然,也有例外情況,那就是我們的這個查詢在索引中就可以完成,也就是說當隻取 a,b兩個字段的時候,不需要回表,而c這個字段不在使用的索引中,需要回表取得其資料。在這樣的情況下,二者的IO量會有較大差異。
-
order by 一定需要排序操作
我們知道索引資料實際上是有序的,如果我們的需要的資料和某個索引的順序一緻,而且我們的查詢又通過這個索引來執行,那麼資料庫一般會省略排序操作,而直接将資料傳回,因為資料庫知道資料已經滿足我們的排序需求了。
實際上,利用索引來優化有排序需求的 SQL,是一個非常重要的優化手段
延伸閱讀:MySQL ORDER BY 的實作分析 ,MySQL 中 GROUP BY 基本實作原理 以及 MySQL DISTINCT 的基本實作原理 這3篇文章中有更為深入的分析,尤其是第一篇
-
執行計劃中有 filesort 就會進行磁盤檔案排序
有這個誤區其實并不能怪我們,而是因為 MySQL 開發者在用詞方面的問題。filesort 是我們在使用 explain 指令檢視一條 SQL 的執行計劃的時候可能會看到在 “Extra” 一列顯示的資訊。
實際上,隻要一條 SQL 語句需要進行排序操作,都會顯示“Using filesort”,這并不表示就會有檔案排序操作。
延伸閱讀:了解 MySQL Explain 指令輸出中的filesort,我在這裡有更為詳細的介紹
-
- 基本原則
-
盡量少 join
MySQL 的優勢在于簡單,但這在某些方面其實也是其劣勢。MySQL 優化器效率高,但是由于其統計資訊的量有限,優化器工作過程出現偏差的可能性也就更多。對于複雜的多表 Join,一方面由于其優化器受限,再者在 Join 這方面所下的功夫還不夠,是以性能表現離 Oracle 等關系型資料庫前輩還是有一定距離。但如果是簡單的單表查詢,這一差距就會極小甚至在有些場景下要優于這些資料庫前輩。
-
盡量少排序
排序操作會消耗較多的 CPU 資源,是以減少排序可以在緩存命中率高等 IO 能力足夠的場景下會較大影響 SQL 的響應時間。
對于MySQL來說,減少排序有多種辦法,比如:
- 上面誤區中提到的通過利用索引來排序的方式進行優化
- 減少參與排序的記錄條數
- 非必要不對資料進行排序
- 避免使用耗費資源的操作,帶有DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的SQL語句會啟動SQL引擎 執行,耗費資源的排序(SORT)功能. DISTINCT需要一次排序操作, 而其他的至少需要執行兩次排序
- …
-
盡量避免 select *
很多人看到這一點後覺得比較難了解,上面不是在誤區中剛剛說 select 子句中字段的多少并不會影響到讀取的資料嗎?
是的,大多數時候并不會影響到 IO 量,但是當我們還存在 order by 操作的時候,select 子句中的字段多少會在很大程度上影響到我們的排序效率,這一點可以通過我之前一篇介紹 MySQL ORDER BY 的實作分析 的文章中有較為詳細的介紹。
此外,上面誤區中不是也說了,隻是大多數時候是不會影響到 IO 量,當我們的查詢結果僅僅隻需要在索引中就能找到的時候,還是會極大減少 IO 量的。
-
盡量用 join 代替子查詢
雖然 Join 性能并不佳,但是和 MySQL 的子查詢比起來還是有非常大的性能優勢。MySQL 的子查詢執行計劃一直存在較大的問題,雖然這個問題已經存在多年,但是到目前已經釋出的所有穩定版本中都普遍存在,一直沒有太大改善。雖然官方也在很早就承認這一問題,并且承諾盡快解決,但是至少到目前為止我們還沒有看到哪一個版本較好的解決了這一問題。
-
盡量少 or
當 where 子句中存在多個條件以“或”并存的時候,MySQL 的優化器并沒有很好的解決其執行計劃優化問題,再加上 MySQL 特有的 SQL 與 Storage 分層架構方式,造成了其性能比較低下,很多時候使用 union all 或者是union(必要的時候)的方式來代替“or”會得到更好的效果。
-
盡量用 union all 代替 union
union 和 union all 的差異主要是前者需要将兩個(或者多個)結果集合并後再進行唯一性過濾操作,這就會涉及到排序,增加大量的 CPU 運算,加大資源消耗及延遲。是以當我們可以确認不可能出現重複結果集或者不在乎重複結果集的時候,盡量使用 union all 而不是 union。
-
盡量早過濾
這一優化政策其實最常見于索引的優化設計中(将過濾性更好的字段放得更靠前)。
在 SQL 編寫中同樣可以使用這一原則來優化一些 Join 的 SQL。比如我們在多個表進行分頁資料查詢的時候,我們最好是能夠在一個表上先過濾好資料分好頁,然後再用分好頁的結果集與另外的表 Join,這樣可以盡可能多的減少不必要的 IO 操作,大大節省 IO 操作所消耗的時間。
-
避免類型轉換
這裡所說的“類型轉換”是指 where 子句中出現 column 字段的類型和傳入的參數類型不一緻的時候發生的類型轉換:
-
人為在column_name 上通過轉換函數進行轉換
直接導緻 MySQL(實際上其他資料庫也會有同樣的問題)無法使用索引,如果非要轉換,應該在傳入的參數上進行轉換
-
SELECT emp.ename, emp.job FROM emp WHERE emp.empno = 7369; 不要使用:SELECT emp.ename, emp.job FROM emp WHERE emp.empno = ‘7369
-
由資料庫自己進行轉換
如果我們傳入的資料類型和字段類型不一緻,同時我們又沒有做任何類型轉換處理,MySQL 可能會自己對我們的資料進行類型轉換操作,也可能不進行處理而交由存儲引擎去處理,這樣一來,就會出現索引無法使用的情況而造成執行計劃問題。
-
-
優先優化高并發的 SQL,而不是執行頻率低某些“大”SQL
對于破壞性來說,高并發的 SQL 總是會比低頻率的來得大,因為高并發的 SQL 一旦出現問題,甚至不會給我們任何喘息的機會就會将系統壓跨。而對于一些雖然需要消耗大量 IO 而且響應很慢的 SQL,由于頻率低,即使遇到,最多就是讓整個系統響應慢一點,但至少可能撐一會兒,讓我們有緩沖的機會。
-
從全局出發優化,而不是片面調整
SQL 優化不能是單獨針對某一個進行,而應充分考慮系統中所有的 SQL,尤其是在通過調整索引優化 SQL 的執行計劃的時候,千萬不能顧此失彼,因小失大。
-
盡可能對每一條運作在資料庫中的SQL進行 explain
優化 SQL,需要做到心中有數,知道 SQL 的執行計劃才能判斷是否有優化餘地,才能判斷是否存在執行計劃問題。在對資料庫中運作的 SQL 進行了一段時間的優化之後,很明顯的問題 SQL 可能已經很少了,大多都需要去發掘,這時候就需要進行大量的 explain 操作收集執行計劃,并判斷是否需要進行優化。
-
二、MySQL 資料庫性能優化之表結構
很多人都将 資料庫設計範式 作為資料庫表結構設計“聖經”,認為隻要按照這個範式需求設計,就能讓設計出來的表結構足夠優化,既能保證性能優異同時還能滿足擴充性要求。殊不知,在N年前被奉為“聖經”的資料庫設計3範式早就已經不完全适用了。這裡我整理了一些比較常見的資料庫表結構設計方面的優化技巧,希望對大家有用。由于MySQL資料庫是基于行(Row)存儲的資料庫,而資料庫操作 IO 的時候是以 page(block)的方式,也就是說,如果我們每條記錄所占用的空間量減小,就會使每個page中可存放的資料行數增大,那麼每次 IO 可通路的行數也就增多了。反過來說,處理相同行數的資料,需要通路的 page 就會減少,也就是 IO 操作次數降低,直接提升性能。此外,由于我們的記憶體是有限的,增加每個page中存放的資料行數,就等于增加每個記憶體塊的緩存資料量,同時還會提升記憶體換中資料命中的幾率,也就是緩存命中率。
-
資料類型選擇
資料庫操作中最為耗時的操作就是 IO 處理,大部分資料庫操作 90% 以上的時間都花在了 IO 讀寫上面。是以盡可能減少 IO 讀寫量,可以在很大程度上提高資料庫操作的性能。我們無法改變資料庫中需要存儲的資料,但是我們可以在這些資料的存儲方式方面花一些心思。下面的這些關于字段類型的優化建議主要适用于記錄條數較多,資料量較大的場景,因為精細化的資料類型設定可能帶來維護成本的提高,過度優化也可能會帶來其他的問題:
- 數字類型:非萬不得已不要使用DOUBLE,不僅僅隻是存儲長度的問題,同時還會存在精确性的問題。同樣,固定精度的小數,也不建議使用DECIMAL,建議乘以固定倍數轉換成整數存儲,可以大大節省存儲空間,且不會帶來任何附加維護成本。對于整數的存儲,在資料量較大的情況下,建議區分開 TINYINT / INT / BIGINT 的選擇,因為三者所占用的存儲空間也有很大的差别,能确定不會使用負數的字段,建議添加unsigned定義。當然,如果資料量較小的資料庫,也可以不用嚴格區分三個整數類型。
- 字元類型:非萬不得已不要使用 TEXT 資料類型,其處理方式決定了他的性能要低于char或者是varchar類型的處理。定長字段,建議使用 CHAR 類型,不定長字段盡量使用 VARCHAR,且僅僅設定适當的最大長度,而不是非常随意的給一個很大的最大長度限定,因為不同的長度範圍,MySQL也會有不一樣的存儲處理。
- 時間類型:盡量使用TIMESTAMP類型,因為其存儲空間隻需要 DATETIME 類型的一半。對于隻需要精确到某一天的資料類型,建議使用DATE類型,因為他的存儲空間隻需要3個位元組,比TIMESTAMP還少。不建議通過INT類型類存儲一個unix timestamp 的值,因為這太不直覺,會給維護帶來不必要的麻煩,同時還不會帶來任何好處。
- ENUM & SET:對于狀态字段,可以嘗試使用 ENUM 來存放,因為可以極大的降低存儲空間,而且即使需要增加新的類型,隻要增加于末尾,修改結構也不需要重建表資料。如果是存放可預先定義的屬性資料呢?可以嘗試使用SET類型,即使存在多種屬性,同樣可以遊刃有餘,同時還可以節省不小的存儲空間。
- LOB類型:強烈反對在資料庫中存放 LOB 類型資料,雖然資料庫提供了這樣的功能,但這不是他所擅長的,我們更應該讓合适的工具做他擅長的事情,才能将其發揮到極緻。在資料庫中存儲 LOB 資料就像讓一個多年前在學校學過一點Java的營銷專業人員來寫 Java 代碼一樣。
-
字元編碼
字元集直接決定了資料在MySQL中的存儲編碼方式,由于同樣的内容使用不同字元集表示所占用的空間大小會有較大的差異,是以通過使用合适的字元集,可以幫助我們盡可能減少資料量,進而減少IO操作次數。
- 純拉丁字元能表示的内容,沒必要選擇 latin1 之外的其他字元編碼,因為這會節省大量的存儲空間
- 如果我們可以确定不需要存放多種語言,就沒必要非得使用UTF8或者其他UNICODE字元類型,這回造成大量的存儲空間浪費
- MySQL的資料類型可以精确到字段,是以當我們需要大型資料庫中存放多位元組資料的時候,可以通過對不同表不同字段使用不同的資料類型來較大程度減小資料存儲量,進而降低 IO 操作次數并提高緩存命中率
-
适當拆分
有些時候,我們可能會希望将一個完整的對象對應于一張資料庫表,這對于應用程式開發來說是很有好的,但是有些時候可能會在性能上帶來較大的問題。當我們的表中存在類似于 TEXT 或者是很大的 VARCHAR類型的大字段的時候,如果我們大部分通路這張表的時候都不需要這個字段,我們就該義無反顧的将其拆分到另外的獨立表中,以減少常用資料所占用的存儲空間。這樣做的一個明顯好處就是每個資料塊中可以存儲的資料條數可以大大增加,既減少實體 IO 次數,也能大大提高記憶體中的緩存命中率。
-
适度備援
為什麼我們要備援?這不是增加了每條資料的大小,減少了每個資料塊可存放記錄條數嗎?确實,這樣做是會增大每條記錄的大小,降低每條記錄中可存放資料的條數,但是在有些場景下我們仍然還是不得不這樣做:
-
被頻繁引用且隻能通過 Join 2張(或者更多)大表的方式才能得到的獨立小字段
這樣的場景由于每次Join僅僅隻是為了取得某個小字段的值,Join到的記錄又大,會造成大量不必要的 IO,完全可以通過空間換取時間的方式來優化。不過,備援的同時需要確定資料的一緻性不會遭到破壞,確定更新的同時備援字段也被更新
-
-
盡量使用 NOT NULL
NULL 類型比較特殊,SQL 難優化。雖然 MySQL NULL類型和 Oracle 的NULL 有差異,會進入索引中,但如果是一個組合索引,那麼這個NULL 類型的字段會極大影響整個索引的效率。此外,NULL 在索引中的處理也是特殊的,也會占用額外的存放空間。
很多人覺得 NULL 會節省一些空間,是以盡量讓NULL來達到節省IO的目的,但是大部分時候這會适得其反,雖然空間上可能确實有一定節省,倒是帶來了很多其他的優化問題,不但沒有将IO量省下來,反而加大了SQL的IO量。是以盡量確定 DEFAULT 值不是 NULL,也是一個很好的表結構設計優化習慣。
三、MySQL 資料庫性能優化之索引優化
大家都知道索引對于資料通路的性能有非常關鍵的作用,都知道索引可以提高資料通路效率。為什麼索引能提高資料通路性能?他會不會有“副作用”?是不是索引建立越多,性能就越好?到底該如何設計索引,才能最大限度的發揮其效能?這篇文章主要是帶着上面這幾個問題來做一個簡要的分析,同時排除了業務場景所帶來的特殊性,請不要糾結業務場景的影響。
-
索引為什麼能提高資料通路性能?
很多人隻知道索引能夠提高資料庫的性能,但并不是特别了解其原理,其實我們可以用一個生活中的示例來了解。我們讓一位不太懂計算機的朋友去圖書館确認一本叫做《MySQL性能調優與架構設計》的書是否在藏,這樣對他說:“請幫我借一本計算機類的資料庫書籍,是屬于 MySQL 資料庫範疇的,叫做《MySQL性能調優與架構設計》”。朋友會根據所屬類别,前往存放“計算機”書籍區域的書架,然後再尋找“資料庫”類存放位置,再找到一堆講述“MySQL”的書籍,最後可能發現目标在藏(也可能已經借出不在書架上)。在這個過程中: “計算機”->“資料庫”->“MySQL”->“在藏”->《MySQL性能調優與架構設計》其實就是一個“根據索引查找資料”的典型案例,“計算機”->“資料庫”->“MySQL”->“在藏” 就是朋友查找書籍的索引。假設沒有這個索引,那查找這本書的過程會變成怎樣呢?朋友隻能從圖書館入口一個書架一個書架的“周遊”,直到找到《MySQL性能調優與架構設計》這本書為止。如果幸運,可能在第一個書架就找到。但如果不幸呢,那就慘了,可能要将整個圖書館所有的書架都找一遍才能找到我們想要的這本書。注:這個例子中的“索引”是記錄在朋友大腦中的,實際上,每個圖書館都會有一個非常全的實際存在的索引系統(大多位于入口顯眼處),由很多個貼上了明顯标簽的小抽屜構成。這個索引系統中存放這非常齊全詳盡的索引資料,辨別出我們需要查找的“目标”在某個區域的某個書架上。而且每當有新的書籍入庫,舊的書籍銷毀以及書記資訊修改,都需要對索引系統進行及時的修正。
- 索引有哪些“副作用”?
- 圖書的變更(增,删,改)都需要修訂索引,索引存在額外的維護成本
- 查找翻閱索引系統需要消耗時間,索引存在額外的通路成本
- 這個索引系統需要一個地方來存放,索引存在額外的空間成本
- 索引是不是越多越好?
-
如果我們的這個圖書館隻是一個進出中轉站,裡面的新書進來後很快就會轉發去其他圖書館而從這個館藏中“清除”,那我們的索引就隻會不斷的修改,而很少會被用來查找圖書
是以,對于類似于這樣的存在非常大更新量的資料,索引的維護成本會非常高,如果其檢索需求很少,而且對檢索效率并沒有非常高的要求的時候,我們并不建議建立索引,或者是盡量減少索引。
-
如果我們的書籍量少到隻有幾本或者就隻有一個書架,索引并不會帶來什麼作用,甚至可能還會浪費一些查找索引所花費的時間。
是以,對于資料量極小到通過索引檢索還不如直接周遊來得快的資料,也并不适合使用索引。
-
如果我們的圖書館隻有一個10平方的面積,現在連放書架都已經非常擁擠,而且館藏還在不斷增加,我們還能考慮建立索引嗎?
是以,當我們連存儲基礎資料的空間都捉襟見肘的時候,我們也應該盡量減少低效或者是去除索引。
-
- 索引該如何設計才高效?
-
如果我們僅僅隻是這樣告訴對方的:“幫我确認一本資料庫類别的講述 MySQL 的叫做《MySQL性能調優與架構設計》的書是否在藏”,結果又會如何呢?朋友隻能一個大類區域一個大類區域的去尋找“資料庫”類别,然後再找到 “MySQL”範疇,再看到我們所需是否在藏。由于我們少說了一個“計算機類”,朋友就必須到每一個大類去尋找。
是以,我們應該盡量讓查找條件盡可能多的在索引中,盡可能通過索引完成所有過濾,回表隻是取出額外的資料字段。
-
如果我們是這樣說的:“幫我确認一本講述 MySQL 的資料庫範疇的計算機叢書,叫做《MySQL性能調優與架構設計》,看是否在藏”。如果這位朋友并不知道計算機是一個大類,也不知道資料庫屬于計算機大類,那這位朋友就悲劇了。首先他得周遊每個類别确認“MySQL”存在于哪些類别中,然後從包含 “MySQL” 書籍中再看有哪些是“資料庫”範疇的(有可能部分是講述PHP或者其他開發語言的),然後再排除非計算機類的(雖然可能并沒有必要),然後才能确認。
是以,字段的順序對組合索引效率有至關重要的作用,過濾效果越好的字段需要更靠前。
-
如果我們還有這樣一個需求(雖然基本不可能):“幫我将圖書館中所有的計算機圖書借來”。朋友如果通過索引來找,每次都到索引櫃找到計算機書籍所在的區域,然後從書架上搬下一格(假設隻能以一格為機關從書架上取下,類比資料庫中以block/page為機關讀取),取出第一本,然後再從索引櫃找到計算機圖書所在區域,再搬下一格,取出一本… 如此往複直至取完所有的書。如果他不通過索引來找又會怎樣呢?他需要從地一個書架一直往後找,當找到計算機的書,搬下一格,取出所有計算機的書,再往後,直至所有書架全部看一遍。在這個過程中,如果計算機類書籍較多,通過索引來取所花費的時間很可能要大于直接周遊,因為不斷往複的索引翻閱所消耗的時間會非常長。(延伸閱讀:這裡有一篇以前寫的關于Oracle的文章,索引掃描還是全表掃描(Index Scan Or Full Table Scan))
是以,當我們需要讀取的資料量占整個資料量的比例較大抑或者說索引的過濾效果并不是太好的時候,使用索引并不一定優于全表掃描。
-
如果我們的朋友不知道“資料庫”這個類别可以屬于“計算機”這個大類,抑或者圖書館的索引系統中這兩個類别屬性并沒有關聯關系,又會怎樣呢?也就是說,朋友得到的是2個獨立的索引,一個是告知“計算機”這個大類所在的區域,一個是“資料庫”這個小類所在的區域(很可能是多個區域),那麼他隻能二者選其一來搜尋我的需求。即使朋友可以分别通過2個索引檢索然後自己在腦中取交集再找,那這樣的效率實際過程中也會比較低下。
是以,在實際使用過程中,一次資料通路一般隻能利用到1個索引,這一點在索引建立過程中一定要注意,不是說一條SQL語句中Where子句裡面每個條件都有索引能對應上就可以了。
- 最後總結一下法則:不要在建立的索引的資料列上進行下列操作: ◆避免對索引字段進行計算操作◆避免在索引字段上使用not,,!=◆避免在索引列上使用IS NULL和IS NOT NULL◆避免在索引列上出現資料類型轉換◆避免在索引字段上使用函數◆避免建立索引的列中使用空值。
-
四、MySQL 資料庫性能優化之緩存參數優化
資料庫屬于 IO 密集型的應用程式,其主要職責就是資料的管理及存儲工作。而我們知道,從記憶體中讀取一個資料庫的時間是微秒級别,而從一塊普通硬碟上讀取一個IO是在毫秒級别,二者相差3個數量級。是以,要優化資料庫,首先第一步需要優化的就是 IO,盡可能将磁盤IO轉化為記憶體IO。本文先從 MySQL 資料庫IO相關參數(緩存參數)的角度來看看可以通過哪些參數進行IO優化:
- query_cache_size/query_cache_type (global) Query cache 作用于整個 MySQL Instance,主要用來緩存 MySQL 中的 ResultSet,也就是一條SQL語句執行的結果集,是以僅僅隻能針對select語句。當我們打開了 Query Cache 功能,MySQL在接受到一條select語句的請求後,如果該語句滿足Query Cache的要求(未顯式說明不允許使用Query Cache,或者已經顯式申明需要使用Query Cache),MySQL 會直接根據預先設定好的HASH算法将接受到的select語句以字元串方式進行hash,然後到Query Cache 中直接查找是否已經緩存。也就是說,如果已經在緩存中,該select請求就會直接将資料傳回,進而省略了後面所有的步驟(如 SQL語句的解析,優化器優化以及向存儲引擎請求資料等),極大的提高性能。當然,Query Cache 也有一個緻命的缺陷,那就是當某個表的資料有任何任何變化,都會導緻所有引用了該表的select語句在Query Cache 中的緩存資料失效。是以,當我們的資料變化非常頻繁的情況下,使用Query Cache 可能會得不償失。Query Cache的使用需要多個參數配合,其中最為關鍵的是 query_cache_size 和 query_cache_type ,前者設定用于緩存 ResultSet 的記憶體大小,後者設定在何場景下使用 Query Cache。在以往的經驗來看,如果不是用來緩存基本不變的資料的MySQL資料庫,query_cache_size 一般 256MB 是一個比較合适的大小。當然,這可以通過計算Query Cache的命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))來進行調整。query_cache_type可以設定為0(OFF),1(ON)或者2(DEMOND),分别表示完全不使用query cache,除顯式要求不使用query cache(使用sql_no_cache)之外的所有的select都使用query cache,隻有顯示要求才使用query cache(使用sql_cache)。
- binlog_cache_size (global) Binlog Cache 用于在打開了二進制日志(binlog)記錄功能的環境,是 MySQL 用來提高binlog的記錄效率而設計的一個用于短時間内臨時緩存binlog資料的記憶體區域。一般來說,如果我們的資料庫中沒有什麼大事務,寫入也不是特别頻繁,2MB~4MB是一個合适的選擇。但是如果我們的資料庫大事務較多,寫入量比較大,可與适當調高binlog_cache_size。同時,我們可以通過binlog_cache_use 以及 binlog_cache_disk_use來分析設定的binlog_cache_size是否足夠,是否有大量的binlog_cache由于記憶體大小不夠而使用臨時檔案(binlog_cache_disk_use)來緩存了。
- key_buffer_size (global) Key Buffer 可能是大家最為熟悉的一個 MySQL 緩存參數了,尤其是在 MySQL 沒有更換預設存儲引擎的時候,很多朋友可能會發現,預設的 MySQL 配置檔案中設定最大的一個記憶體參數就是這個參數了。key_buffer_size 參數用來設定用于緩存 MyISAM存儲引擎中索引檔案的記憶體區域大小。如果我們有足夠的記憶體,這個緩存區域最好是能夠存放下我們所有的 MyISAM 引擎表的所有索引,以盡可能提高性能。此外,當我們在使用MyISAM 存儲的時候有一個及其重要的點需要注意,由于 MyISAM 引擎的特性限制了他僅僅隻會緩存索引塊到記憶體中,而不會緩存表資料庫塊。是以,我們的 SQL 一定要盡可能讓過濾條件都在索引中,以便讓緩存幫助我們提高查詢效率。
-
bulk_insert_buffer_size (thread)和key_buffer_size一樣,這個參數同樣也僅作用于使用 MyISAM存儲引擎,用來緩存批量插入資料的時候臨時緩存寫入資料。當我們使用如下幾種資料寫入語句的時候,會使用這個記憶體區域來緩存批量結構的資料以幫助批量寫入資料檔案:insert … select …
insert … values (…) ,(…),(…)…
load data infile… into… (非空表)
- innodb_buffer_pool_size(global)當我們使用InnoDB存儲引擎的時候,innodb_buffer_pool_size 參數可能是影響我們性能的最為關鍵的一個參數了,他用來設定用于緩存 InnoDB 索引及資料塊的記憶體區域大小,類似于 MyISAM 存儲引擎的 key_buffer_size 參數,當然,可能更像是 Oracle 的 db_cache_size。簡單來說,當我們操作一個 InnoDB 表的時候,傳回的所有資料或者去資料過程中用到的任何一個索引塊,都會在這個記憶體區域中走一遭。和key_buffer_size 對于 MyISAM 引擎一樣,innodb_buffer_pool_size 設定了 InnoDB 存儲引擎需求最大的一塊記憶體區域的大小,直接關系到 InnoDB存儲引擎的性能,是以如果我們有足夠的記憶體,盡可将該參數設定到足夠打,将盡可能多的 InnoDB 的索引及資料都放入到該緩存區域中,直至全部。我們可以通過 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100% 計算緩存命中率,并根據命中率來調整 innodb_buffer_pool_size 參數大小進行優化。
- innodb_additional_mem_pool_size(global)這個參數我們平時調整的可能不是太多,很多人都使用了預設值,可能很多人都不是太熟悉這個參數的作用。innodb_additional_mem_pool_size 設定了InnoDB存儲引擎用來存放資料字典資訊以及一些内部資料結構的記憶體空間大小,是以當我們一個MySQL Instance中的資料庫對象非常多的時候,是需要适當調整該參數的大小以確定所有資料都能存放在記憶體中提高通路效率的。這個參數大小是否足夠還是比較容易知道的,因為當過小的時候,MySQL 會記錄 Warning 資訊到資料庫的 error log 中,這時候你就知道該調整這個參數大小了。
-
innodb_log_buffer_size (global)這是 InnoDB 存儲引擎的事務日志所使用的緩沖區。類似于 Binlog Buffer,InnoDB 在寫事務日志的時候,為了提高性能,也是先将資訊寫入 Innofb Log Buffer 中,當滿足 innodb_flush_log_trx_commit 參數所設定的相應條件(或者日志緩沖區寫滿)之後,才會将日志寫到檔案(或者同步到磁盤)中。可以通過 innodb_log_buffer_size 參數設定其可以使用的最大記憶體空間。
注:innodb_flush_log_trx_commit 參數對 InnoDB Log 的寫入性能有非常關鍵的影響。該參數可以設定為0,1,2,解釋如下:0:log buffer中的資料将以每秒一次的頻率寫入到log file中,且同時會進行檔案系統到磁盤的同步操作,但是每個事務的commit并不會觸發任何log buffer 到log file的重新整理或者檔案系統到磁盤的重新整理操作;
1:在每次事務送出的時候将log buffer 中的資料都會寫入到log file,同時也會觸發檔案系統到磁盤的同步;
2:事務送出會觸發log buffer 到log file的重新整理,但并不會觸發磁盤檔案系統到磁盤的同步。此外,每秒會有一次檔案系統到磁盤同步操作。此外,MySQL文檔中還提到,這幾種設定中的每秒同步一次的機制,可能并不會完全確定非常準确的每秒就一定會發生同步,還取決于程序排程的問題。實際上,InnoDB 能否真正滿足此參數所設定值代表的意義正常 Recovery 還是受到了不同 OS 下檔案系統以及磁盤本身的限制,可能有些時候在并沒有真正完成磁盤同步的情況下也會告訴 mysqld 已經完成了磁盤同步。
- innodb_max_dirty_pages_pct (global)這個參數和上面的各個參數不同,他不是用來設定用于緩存某種資料的記憶體大小的一個參數,而是用來控制在 InnoDB Buffer Pool 中可以不用寫入資料檔案中的Dirty Page 的比例(已經被修但還沒有從記憶體中寫入到資料檔案的髒資料)。這個比例值越大,從記憶體到磁盤的寫入操作就會相對減少,是以能夠一定程度下減少寫入操作的磁盤IO。但是,如果這個比例值過大,當資料庫 Crash 之後重新開機的時間可能就會很長,因為會有大量的事務資料需要從日志檔案恢複出來寫入資料檔案中。同時,過大的比例值同時可能也會造成在達到比例設定上限後的 flush 操作“過猛”而導緻性能波動很大。
- query_cache_type : 如果全部使用innodb存儲引擎,建議為0,如果使用MyISAM 存儲引擎,建議為2,同時在SQL語句中顯式控制是否是喲你gquery cache
- query_cache_size: 根據 命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))進行調整,一般不建議太大,256MB可能已經差不多了,大型的配置型靜态資料可适當調大
- binlog_cache_size: 一般環境2MB~4MB是一個合适的選擇,事務較大且寫入頻繁的資料庫環境可以适當調大,但不建議超過32MB
- key_buffer_size: 如果不使用MyISAM存儲引擎,16MB足以,用來緩存一些系統表資訊等。如果使用 MyISAM存儲引擎,在記憶體允許的情況下,盡可能将所有索引放入記憶體,簡單來說就是“越大越好”
- bulk_insert_buffer_size: 如果經常性的需要使用批量插入的特殊語句(上面有說明)來插入資料,可以适當調大該參數至16MB~32MB,不建議繼續增大,某人8MB
- innodb_buffer_pool_size: 如果不使用InnoDB存儲引擎,可以不用調整這個參數,如果需要使用,在記憶體允許的情況下,盡可能将所有的InnoDB資料檔案存放如記憶體中,同樣将但來說也是“越大越好”
- innodb_additional_mem_pool_size: 一般的資料庫建議調整到8MB~16MB,如果表特别多,可以調整到32MB,可以根據error log中的資訊判斷是否需要增大
- innodb_log_buffer_size: 預設是1MB,系的如頻繁的系統可适當增大至4MB~8MB。當然如上面介紹所說,這個參數實際上還和另外的flush參數相關。一般來說不建議超過32MB
- innodb_max_dirty_pages_pct: 根據以往的經驗,重新開機恢複的資料如果要超過1GB的話,啟動速度會比較慢,幾乎難以接受,是以建議不大于 1GB/innodb_buffer_pool_size(GB)*100 這個值。當然,如果你能夠忍受啟動時間比較長,而且希望盡量減少記憶體至磁盤的flush,可以将這個值調整到90,但不建議超過90
Mysql優化總結
一、索引
1、建立索引:
(1).ALTER TABLE
ALTER TABLE用來建立普通索引、UNIQUE索引或PRIMARY KEY索引。
ALTER TABLE table_name ADD INDEX index_name (column_list)
ALTER TABLE table_name ADD UNIQUE (column_list)
ALTER TABLE table_name ADD PRIMARY KEY (column_list)
(2)、CREATE INDEX
CREATE INDEX可對表增加普通索引或UNIQUE索引。
CREATE INDEX index_name ON table_name (column_list)
CREATE UNIQUE INDEX index_name ON table_name (column_list)
2、檢視索引
mysql> show index from tblname;
mysql> show keys from tblname;
3、删除索引
可利用ALTER TABLE或DROP INDEX語句來删除索引。類似于CREATE INDEX語句,DROP INDEX可以在ALTER TABLE 内部作為一條語句處理,文法如下。
DROP INDEX index_name ON talbe_name
ALTER TABLE table_name DROP INDEX index_name
ALTER TABLE table_name DROP PRIMARY KEY
索引:http://www.cnblogs.com/hustcat/archive/2009/10/28/1591648.html
**explain +select ·····用來擷取select語句的執行的相關資訊及索引的使用等
**describe table table_name;
**analyze table table_name;檢視表的資訊,幫助優化
**show 檢視執行狀态
二、my.ini中的配置
http://www.chinaz.com/program/2009/1210/100740.shtml
mysql > show status; 可以檢視具體的設定 伺服器的狀态
具體的配置呀什麼,沒有親自試驗過
三、資料表引擎
1、MyISAM:mysql預設的
2、InnoDB:支援事務、鎖、外鍵、聚簇索引
引擎介紹:http://blog.csdn.net/cheungjustin/article/details/5999880
http://limaolinjia.blog.163.com/blog/static/539162282011012145139/
四、索引的類型:
1、B-Tree索引
2、hash索引
具體的參考還是一)
五、事務
資料表引擎使用InnoDB
http://www.cnblogs.com/winner/archive/2011/11/09/2242272.html
六、存儲過程
經編譯和優化後存儲在資料庫伺服器中,運作效率高,可以降低客戶機和伺服器之間的通信量,有利于集中控制,易于維護 (P247)
http://blog.sina.com.cn/s/blog_52d20fbf0100ofd5.html
七、mysql profiling(mysql性能分析器)優化sql語句
檢視SQL執行消耗系統資源的資訊
++++需要開啟+++
具體使用:http://www.jiunile.com/mysql-profiling%E7%9A%84%E4%BD%BF%E7%94%A8.html
八、慢查詢日志
++++需要開啟++++
通過慢日志查詢可以知道哪些SQL語句執行效率低下,那些sql語句使用的頻率高等
對MySQL查詢語句的監控、分析、優化是MySQL優化非常重要的一步。開啟慢查詢日志後,由于日志記錄操作,在一定程度上會占用CPU資源影響mysql的性能,但是可以階段性開啟來定位性能瓶頸。
具體參考:http://blog.csdn.net/renzhenhuai/article/details/8839874
關于mysql的一些講解:http://www.ccvita.com/category/mysql
-