面向程式員的資料庫訪問性能優化法則 特別說明: 1、 本文只是面對資料庫應用開發的程式員,不適合專業DBA,DBA在資料庫性能優化方面需要瞭解更多的知識; 2、 本文許多示例及概念是基於Oracle資料庫描述,對於其它關係型資料庫也可以參考,但許多觀點不適合於KV資料庫或記憶體資料庫或者是基於SSD技 ...
面向程式員的資料庫訪問性能優化法則
特別說明:
1、 本文只是面對資料庫應用開發的程式員,不適合專業DBA,DBA在資料庫性能優化方面需要瞭解更多的知識;
2、 本文許多示例及概念是基於Oracle資料庫描述,對於其它關係型資料庫也可以參考,但許多觀點不適合於KV資料庫或記憶體資料庫或者是基於SSD技術的資料庫;
3、 本文未深入資料庫優化中最核心的執行計劃分析技術。
讀者對像:
開發人員:如果你是做資料庫開發,那本文的內容非常適合,因為本文是從程式員的角度來談資料庫性能優化。
架構師:如果你已經是資料庫應用的架構師,那本文的知識你應該清楚90%,否則你可能是一個喜歡折騰的架構師。
DBA(資料庫管理員):大型資料庫優化的知識非常複雜,本文只是從程式員的角度來談性能優化,DBA除了需要瞭解這些知識外,還需要深入資料庫的內部體系架構來解決問題。
引言
在網上有很多文章介紹資料庫優化知識,但是大部份文章只是對某個一個方面進行說明,而對於我們程式員來說這種介紹並不能很好的掌握優化知識,因為很多介紹只是對一些特定的場景優化的,所以反而有時會產生誤導或讓程式員感覺不明白其中的奧妙而對資料庫優化感覺很神秘。
很多程式員總是問如何學習資料庫優化,有沒有好的教材之類的問題。在書店也看到了許多資料庫優化的專業書籍,但是感覺更多是面向DBA或者是PL/SQL開發方面的知識,個人感覺不太適合普通程式員。而要想做到資料庫優化的高手,不是花幾周,幾個月就能達到的,這並不是因為資料庫優化有多高深,而是因為要做好優化一方面需要有非常好的技術功底,對操作系統、存儲硬體網路、資料庫原理等方面有比較扎實的基礎知識,另一方面是需要花大量時間對特定的資料庫進行實踐測試與總結。
作為一個程式員,我們也許不清楚線上正式的伺服器硬體配置,我們不可能像DBA那樣專業的對資料庫進行各種實踐測試與總結,但我們都應該非常瞭解我們SQL的業務邏輯,我們清楚SQL中訪問表及欄位的數據情況,我們其實只關心我們的SQL是否能儘快返回結果。那程式員如何利用已知的知識進行資料庫優化?如何能快速定位SQL性能問題並找到正確的優化方向?
面對這些問題,筆者總結了一些面向程式員的基本優化法則,本文將結合實例來坦述資料庫開發的優化知識。
一、資料庫訪問優化法則簡介
要正確的優化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 /*+parallel(a,4)*/ * from employee;
並行的優點:
使用多進程處理,充分利用資料庫主機資源(CPU,IO),提高性能。
並行的缺點:
1、單個會話占用大量資源,影響其它會話,所以只適合在主機負載低時期使用;
2、只能採用直接IO訪問,不能利用緩存數據,所以執行前會觸發將臟緩存數據寫入磁碟操作。
註:
1、並行處理在OLTP類系統中慎用,使用不當會導致一個會話把主機資源全部占用,而正常事務得不到及時響應,所以一般只是用於數據倉庫平臺。
2、一般對於百萬級記錄以下的小表採用並行訪問性能並不能提高,反而可能會讓性能更差。
摘自:https://blog.csdn.net/yzsind/article/details/6059209