數據庫訪問性能優化(二)

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性能。

目前ORACLESQL執行計劃的算法約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個產品,假設訪問公司索引需要50IO2條記錄需要1個表數據IO

那麼按第一種ROWNUM分頁寫法,需要550(50+1000/2)IO,按第二種ROWID分頁寫法,只需要60IO(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_FILEID,FILE_NAME,FILE_SIZE,FILE_TYPE,FILE_CONTENT

我們可以分拆成兩張一對一的關係表:

T_FILEID,FILE_NAME,FILE_SIZE,FILE_TYPE

T_FILECONTENTID, FILE_CONTENT

         通過這種分拆,可以大大提少T_FILE表的單條記錄及總大小,這樣在查詢T_FILE時性能會更好,當需要查詢FILE_CONTENT字段內容時再訪問T_FILECONTENT表。

 

3、減少交互次數

3.1batch 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倍性能,一般根據主鍵的UpdateDelete操作也可能提高2-3倍性能,但不如Insert明顯,因爲UpdateDelete操作可能有比較大的開銷在物理IO訪問。以上僅是理論計算值,實際情況需要根據具體環境測量。

 

3.2In 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請求的數量,從而提高性能。那如果有10000ID,那是不是全部放在一條SQL裏處理呢?答案肯定是否定的。首先大部份數據庫都會有SQL長度和IN裏個數的限制,如ORACLEIN裏就不允許超過1000個值

另外當前數據庫一般都是採用基於成本的優化規則,當IN數量達到一定值時有可能改變SQL執行計劃,從索引訪問變成全表訪問,這將使性能急劇變化。隨着SQLIN的裏面的值個數增加,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_times

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,太大了也沒什麼性能提高,反而可能會增加內存溢出的危險。

注:圖中fetchsize128以後會有一些小的波動,這並不是測試誤差,而是由於resultset填充到具體對像時間不同的原因,由於resultset已經到本地內存裏了,所以估計是由於CPUL1,L2 Cache命中率變化造成,由於變化不大,所以筆者也未深入分析原因。

 

iBatisSqlMapping配置文件可以對每個SQL語句指定fetchsize大小,如下所示:


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