數據庫訪問性能優化

特別說明:html

一、  本文只是面對數據庫應用開發的程序員,不適合專業DBADBA在數據庫性能優化方面須要瞭解更多的知識;java

二、  本文許多示例及概念是基於Oracle數據庫描述,對於其它關係型數據庫也能夠參考,但許多觀點不適合於KV數據庫或內存數據庫或者是基於SSD技術的數據庫;程序員

三、  本文未深刻數據庫優化中最核心的執行計劃分析技術。算法

 

讀者對像:sql

開發人員:若是你是作數據庫開發,那本文的內容很是適合,由於本文是從程序員的角度來談數據庫性能優化。數據庫

架構師:若是你已是數據庫應用的架構師,那本文的知識你應該清楚90%,不然你多是一個喜歡折騰的架構師。編程

DBA(數據庫管理員):大型數據庫優化的知識很是複雜,本文只是從程序員的角度來談性能優化,DBA除了須要瞭解這些知識外,還須要深刻數據庫的內部體系架構來解決問題。瀏覽器

 


引言緩存

在網上有不少文章介紹數據庫優化知識,可是大部份文章只是對某個一個方面進行說明,而對於咱們程序員來講這種介紹並不能很好的掌握優化知識,由於不少介紹只是對一些特定的場景優化的,因此反而有時會產生誤導或讓程序員感受不明白其中的奧妙而對數據庫優化感受很神祕。安全

不少程序員老是問如何學習數據庫優化,有沒有好的教材之類的問題。在書店也看到了許多數據庫優化的專業書籍,可是感受更可能是面向DBA或者是PL/SQL開發方面的知識,我的感受不太適合普通程序員。而要想作到數據庫優化的高手,不是花幾周,幾個月就能達到的,這並非由於數據庫優化有多高深,而是由於要作好優化一方面須要有很是好的技術功底,對操做系統、存儲硬件網絡、數據庫原理等方面有比較紮實的基礎知識,另外一方面是須要花大量時間對特定的數據庫進行實踐測試與總結。

做爲一個程序員,咱們也許不清楚線上正式的服務器硬件配置,咱們不可能像DBA那樣專業的對數據庫進行各類實踐測試與總結,但咱們都應該很是瞭解咱們SQL的業務邏輯,咱們清楚SQL中訪問表及字段的數據狀況,咱們其實只關心咱們的SQL是否能儘快返回結果。那程序員如何利用已知的知識進行數據庫優化?如何能快速定位SQL性能問題並找到正確的優化方向?

面對這些問題,筆者總結了一些面向程序員的基本優化法則,本文將結合實例來坦述數據庫開發的優化知識。

1、數據庫訪問優化法則簡介

要正確的優化SQL,咱們須要快速定位能性的瓶頸點,也就是說快速找到咱們SQL主要的開銷在哪裏?而大多數狀況性能最慢的設備會是瓶頸點,以下載時網絡速度可能會是瓶頸點,本地複製文件時硬盤可能會是瓶頸點,爲何這些通常的工做咱們能快速確認瓶頸點呢,由於咱們對這些慢速設備的性能數據有一些基本的認識,如網絡帶寬是2Mbps,硬盤是每分鐘7200轉等等。所以,爲了快速找到SQL的性能瓶頸點,咱們也須要了解咱們計算機系統的硬件基本性能指標,下圖展現的當前主流計算機性能指標數據。

 

 

從圖上能夠看到基本上每種設備都有兩個指標:

延時(響應時間):表示硬件的突發處理能力;

帶寬(吞吐量):表明硬件持續處理能力。

 

從上圖能夠看出,計算機系統硬件性能從高到代依次爲:

CPU——Cache(L1-L2-L3)——內存——SSD硬盤——網絡——硬盤

因爲SSD硬盤還處於快速發展階段,因此本文的內容不涉及SSD相關應用系統。

根據數據庫知識,咱們能夠列出每種硬件主要的工做內容:

CPU及內存:緩存數據訪問、比較、排序、事務檢測、SQL解析、函數或邏輯運算;

網絡:結果數據傳輸、SQL請求、遠程數據庫訪問(dblink);

硬盤:數據訪問、數據寫入、日誌記錄、大數據量排序、大表鏈接。

 

根據當前計算機硬件的基本性能指標及其在數據庫中主要操做內容,能夠整理出以下圖所示的性能基本優化法則:

  

 

這個優化法則概括爲5個層次:

一、  減小數據訪問(減小磁盤訪問)

二、  返回更少數據(減小網絡傳輸或磁盤訪問)

三、  減小交互次數(減小網絡傳輸)

四、  減小服務器CPU開銷(減小CPU及內存開銷)

五、  利用更多資源(增長資源)

 

因爲每一層優化法則都是解決其對應硬件的性能問題,因此帶來的性能提高比例也不同。傳統數據庫系統設計是也是儘量對低速設備提供優化方法,所以針對低速設備問題的可優化手段也更多,優化成本也更低。咱們任何一個SQL的性能優化都應該按這個規則由上到下來診斷問題並提出解決方案,而不該該首先想到的是增長資源解決問題。

如下是每一個優化法則層級對應優化效果及成本經驗參考:

 

 

優化法則

性能提高效果

優化成本

減小數據訪問

1~1000

返回更少數據

1~100

減小交互次數

1~20

減小服務器CPU開銷

1~5

利用更多資源

@~10

 

 

接下來,咱們針對5種優化法則列舉經常使用的優化手段並結合實例分析。

 

2、Oracle數據庫兩個基本概念

數據塊(Block)

數據塊是數據庫中數據在磁盤中存儲的最小單位,也是一次IO訪問的最小單位,一個數據塊一般能夠存儲多條記錄,數據塊大小是DBA在建立數據庫或表空間時指定,可指定爲2K4K8K16K32K字節。下圖是一個Oracle數據庫典型的物理結構,一個數據庫能夠包括多個數據文件,一個數據文件內又包含多個數據塊;

 

 

ROWID

ROWID是每條記錄在數據庫中的惟一標識,經過ROWID能夠直接定位記錄到對應的文件號及數據塊位置。ROWID內容包括文件號、對像號、數據塊號、記錄槽號,以下圖所示:

 

3、數據庫訪問優化法則詳解

1、減小數據訪問

1.1、建立並使用正確的索引

數據庫索引的原理很是簡單,但在複雜的表中真正能正確使用索引的人不多,即便是專業的DBA也不必定能徹底作到最優。

索引會大大增長表記錄的DML(INSERT,UPDATE,DELETE)開銷,正確的索引可讓性能提高1001000倍以上,不合理的索引也可能會讓性能降低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性能。

目前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大小,以下所示:

 

<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、爲了提升性能,數據庫會把存儲過程代碼編譯成中間運行代碼(相似於javaclass文件),因此更像靜態語言。當存儲過程引用的對像(表、視圖等等)結構改變後,存儲過程須要從新編譯才能生效,在24*7高併發應用場景,通常都是在線變動結構的,因此在變動的瞬間要同時編譯存儲過程,這可能會致使數據庫瞬間壓力上升引發故障(Oracle數據庫就存在這樣的問題)

 

我的觀點:普通業務邏輯儘可能不要使用存儲過程,定時性的ETL任務或報表統計函數能夠根據團隊資源狀況採用存儲過程處理。

 

3.5、優化業務邏輯

要經過優化業務邏輯來提升性能是比較困難的,這須要程序員對所訪問的數據及業務流程很是清楚。

舉一個案例:

某移動公司推出優惠套參,活動對像爲VIP會員而且2010123月平均話費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

    }

}

 

iBatisqueryWithRowHandler很好的封裝了resultset遍歷的事件處理,效果及性能與resultset遍歷同樣,也不會產生JVM內存溢出。

 

4、減小數據庫服務器CPU運算

4.1、使用綁定變量

綁定變量是指SQL中對變化的值採用變量參數的形式提交,而不是在SQL中直接拼寫對應的值。

非綁定變量寫法:Select * from employee where id=1234567

綁定變量寫法:

Select * from employee where id=?

Preparestatement.setInt(1,1234567)

 

JavaPreparestatement就是爲處理綁定變量提供的對像,綁定變量有如下優勢:

1、防止SQL注入

2、提升SQL可讀性

3、提升SQL解析性能,不使用綁定變動咱們通常稱爲硬解析,使用綁定變量咱們稱爲軟解析。

1和第2點很好理解,作編碼的人應該都清楚,這裏不詳細說明。關於第3點,到底能提升多少性能呢,下面舉一個例子說明:

 

假設有這個這樣的一個數據庫主機:

24CPU 

100塊磁盤,每一個磁盤支持IOPS160

業務應用的SQL以下:

select * from table where pk=?

這個SQL平均4IO3個索引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執行原理說明,一條SQLOracle數據庫中的執行過程以下圖所示:

 

 

 

當一條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機作過測試,單核普通CPU1秒鐘能夠完成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=ba<b之類的操做,對於這些比較操做數據庫都體現得很好,可是若是有如下操做,咱們須要保持警戒:

Like模糊查詢,以下所示:

a like ‘%abc%’

 

Like模糊查詢對於數據庫來講不是很擅長,特別是你須要模糊檢查的記錄有上萬條以上時,性能比較糟糕,這種狀況通常能夠採用專用Search或者採用全文索引方案來提升性能。

不能使用索引定位的大量In List,以下所示:

a in (:1,:2,:3,…,:n)   ----n>20

若是這裏的a字段不能經過索引比較,那數據庫會將字段與in裏面的每一個值都進行比較運算,若是記錄數有上萬以上,會明顯感受到SQLCPU開銷加大,這個狀況有兩種解決方式:

a、  in列表裏面的數據放入一張中間小表,採用兩個表Hash Join關聯的方式處理;

b、  採用str2varList方法將字段串列表轉換一個臨時表處理,關於str2varList方法能夠在網上直接查詢,這裏不詳細介紹。

 

以上兩種解決方案都須要與中間表Hash Join的方式才能提升性能,若是採用了Nested Loop的鏈接方式性能會更差。

若是發現咱們的系統IO沒問題可是CPU負載很高,就有多是上面的緣由,這種狀況不太常見,若是遇到了最好能和DBA溝通並確認準確的緣由。

 

4.4、大量複雜運算在客戶端處理

什麼是複雜運算,通常我認爲是一秒鐘CPU只能作10萬次之內的運算。如含小數的對數及指數運算、三角函數、3DESBASE64數據加密算法等等。

若是有大量這類函數運算,儘可能放在客戶端處理,通常CPU每秒中也只能處理1-10萬次這樣的函數運算,放在數據庫內不利於高併發處理。

 

5、利用更多的資源

5.1、客戶端多進程並行訪問

多進程並行訪問是指在客戶端建立多個進程(線程),每一個進程創建一個與數據庫的鏈接,而後同時向數據庫提交訪問請求。當數據庫主機資源有空閒時,咱們能夠採用客戶端多進程並行訪問的方法來提升性能。若是數據庫主機已經很忙時,採用多進程並行訪問性能不會提升,反而可能會更慢。因此使用這種方式最好與DBA或系統管理員進行溝通後再決定是否採用。

 

例如:

咱們有10000個產品ID,如今須要根據ID取出產品的詳細信息,若是單線程訪問,按每一個IO5ms計算,忽略主機CPU運算及網絡傳輸時間,咱們須要50s才能完成任務。若是採用5個並行訪問,每一個進程訪問2000ID,那麼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://www.cnblogs.com/easypass/archive/2010/12/08/1900127.html
相關文章
相關標籤/搜索