1. 程式人生 > 實用技巧 >資料庫系列整理---資料庫訪問優化法則

資料庫系列整理---資料庫訪問優化法則

資料庫訪問優化法則

瞭解計算機系統的硬體基本效能指標,可以快速找到SQL的效能瓶頸點,下面是當前主流計算機效能指標資料。

從圖上可以看到基本上每種裝置都有兩個指標:

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

頻寬(吞吐量):代表硬體持續處理能力。

從上圖可以看出,計算機系統硬體效能從高到代依次為:

CPU——Cache(L1-L2-L3)——記憶體——SSD硬碟——網路——硬碟

根據資料庫知識,我們可以列出每種硬體主要的工作內容:

CPU及記憶體:快取資料訪問、比較、排序、事務檢測、SQL解析、函式或邏輯運算;

網路:結果資料傳輸、SQL請求、遠端資料庫訪問(dblink);

硬碟:資料訪問、資料寫入、日誌記錄、大資料量排序、大表連線。

根據當前計算機硬體的基本效能指標及其在資料庫中主要操作內容,可以整理出如下圖所示的效能基本優化法則:

由於每一層優化法則都是解決其對應硬體的效能問題,所以帶來的效能提升比例也不一樣。傳統資料庫系統設計是也是儘可能對低速裝置提供優化方法,因此針對低速裝置問題的可優化手段也更多,優化成本也更低。我們任何一個SQL的效能優化都應該按這個規則由上到下來診斷問題並提出解決方案,而不應該首先想到的是增加資源解決問題。

以下是5個層次的優化法則對應優化效果及成本經驗參考:

優化法則

做法

效能提升效果

優化成本

減少資料訪問

減少磁碟訪問

1~1000

返回更少資料

減少網路傳輸或磁碟訪問

1~100

減少互動次數

減少網路傳輸

1~20

減少伺服器CPU開銷

減少CPU及記憶體開銷

1~5

利用更多資源

增加資源

@~10

DML資料操作語言(insert delete update select)
DTL資料事務語言(commit rollback savepoint)
DDL資料定義語言(create alter drop..)
DCL資料控制語言(
grant revoke)

資料庫效能優化的點很多,這裡描述的是常用的一些對資料庫效能有所提升的點:
1、資料庫設計
2、SQL語句優化
3、資料庫引數配置
4、恰當的硬體資源和作業系統
5、使用適當的儲存過程 [模組化程式設計,可以提升效能]

資料庫設計

表設計優化

(合理的表名,合理的欄位名(參考 資料庫表、欄位設計

資料庫索引

參考 資料庫索引

選擇合適的儲存引擎

參考 儲存引擎

業務庫分庫、分表技術(水平分割、垂直分割)

SQL語句優化

查詢SQL優化

參考查詢SQL優化

1、資料分頁處理

一般資料分頁方式有:

1.1、客戶端(應用程式或瀏覽器)分頁

將資料從應用伺服器全部下載到本地應用程式或瀏覽器,在應用程式或瀏覽器內部通過原生代碼進行分頁處理

優點:編碼簡單,減少客戶端與應用伺服器網路互動次數

缺點:首次互動時間長,佔用客戶端記憶體

適應場景:客戶端與應用伺服器網路延時較大,但要求後續操作流暢,如手機GPRS,超遠端訪問(跨國)等等。

1.2、應用伺服器分頁

將資料從資料庫伺服器全部下載到應用伺服器,在應用伺服器內部再進行資料篩選。以下是一個應用伺服器端Java程式分頁的示例:

List list=executeQuery(“select * from employee order by id”);

Int count= list.size();

List subList= list.subList(10, 20);

優點:編碼簡單,只需要一次SQL互動,總資料與分頁資料差不多時效能較好。

缺點:總資料量較多時效能較差。

適應場景:資料庫系統不支援分頁處理,資料量較小並且可控。

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.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訪問。以上僅是理論計算值,實際情況需要根據具體環境測量。

2.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評估。

2.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,太大了也沒什麼效能提高,反而可能會增加記憶體溢位的危險。

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

iBatis的SqlMapping配置檔案可以對每個SQL語句指定fetchsize大小,如下所示:

<select id="getAllProduct" resultMap="HashMap" fetchSize="1000">

select * from employee

</select>

2.4、使用儲存過程

大型資料庫一般都支援儲存過程,合理的利用儲存過程也可以提高系統性能。如你有一個業務需要將A表的資料做一些加工然後更新到B表中,但是又不可能一條SQL完成,這時你需要如下3步操作:

a:將A表資料全部取出到客戶端;

b:計算出要更新的資料;

c:將計算結果更新到B表。

如果採用儲存過程你可以將整個業務邏輯封裝在儲存過程裡,然後在客戶端直接呼叫儲存過程處理,這樣可以減少網路互動的成本。

當然,儲存過程也並不是十全十美,儲存過程有以下缺點:

a、不可移植性,每種資料庫的內部程式設計語法都不太相同,當你的系統需要相容多種資料庫時最好不要用儲存過程。

b、學習成本高,DBA一般都擅長寫儲存過程,但並不是每個程式設計師都能寫好儲存過程,除非你的團隊有較多的開發人員熟悉寫儲存過程,否則後期系統維護會產生問題。

c、業務邏輯多處存在,採用儲存過程後也就意味著你的系統有一些業務邏輯不是在應用程式裡處理,這種架構會增加一些系統維護和除錯成本。

d、儲存過程和常用應用程式語言不一樣,它支援的函式及語法有可能不能滿足需求,有些邏輯就只能通過應用程式處理。

e、如果儲存過程中有複雜運算的話,會增加一些資料庫服務端的處理成本,對於集中式資料庫可能會導致系統可擴充套件性問題。

f、為了提高效能,資料庫會把儲存過程程式碼編譯成中間執行程式碼(類似於java的class檔案),所以更像靜態語言。當儲存過程引用的對像(表、檢視等等)結構改變後,儲存過程需要重新編譯才能生效,在24*7高併發應用場景,一般都是線上變更結構的,所以在變更的瞬間要同時編譯儲存過程,這可能會導致資料庫瞬間壓力上升引起故障(Oracle資料庫就存在這樣的問題)。

個人觀點:普通業務邏輯儘量不要使用儲存過程,定時性的ETL任務或報表統計函式可以根據團隊資源情況採用儲存過程處理。

2.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的互動次數。

以上只是一個簡單的示例,實際的業務總是比這複雜得多,所以一般只是高階程式設計師更容易做出優化的邏輯,但是我們需要有這樣一種成本優化的意識。

2.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記憶體溢位。

3、[減少資料庫伺服器CPU運算]

3.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';//使用索引

3.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執行計劃優化專題。

3.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溝通並確認準確的原因。

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

什麼是複雜運算,一般我認為是一秒鐘CPU只能做10萬次以內的運算。如含小數的對數及指數運算、三角函式、3DES及BASE64資料加密演算法等等。

如果有大量這類函式運算,儘量放在客戶端處理,一般CPU每秒中也只能處理1萬-10萬次這樣的函式運算,放在資料庫內不利於高併發處理。

其他:
避免使用兩個不同型別的列進行表的連線
講解:
1、當連線兩個不同型別的列時,其中一個列必須轉換成另一個列的型別,級別低的會被轉換成高級別的型別,轉換操作會消耗一定的系統資源;
2、如果你使用兩個不同型別的列來連線表,其中一個列原本可以使用索引,但經過轉換後,優化器就不會使用它的索引了
select column_list from small_table, large_table where small_table.float_column = large_table.int_column;
在這個例子中,資料庫會將int列轉換為float型別,因為int比float型別的級別低,large_table.int_column上的索引就不會被使用,但smalltable.float_column上的索引可以正常使用。
避免死鎖
1、在你的儲存過程和觸發器中訪問同一個表時總是以相同的順序;
2、事務應儘可能地縮短,在一個事務中應儘可能減少涉及到的資料量;
3、永遠不要在事務中等待使用者輸入。
使用“基於規則的方法”而不是使用“程式化方法”編寫SQL
1、資料庫引擎專門為基於規則的SQL進行了優化,因此處理大型結果集時應儘量避免使用程式化的方法(使用遊標或UDF[User Defined Functions]處理返回的結果集) ;
2、如何擺脫程式化的SQL呢?有以下方法:
使用內聯子查詢替換使用者定義函式;
使用相關聯的子查詢替換基於遊標的程式碼;
如果確實需要程式化程式碼,至少應該使用表變數代替遊標導航和處理結果集。
避免使用動態SQL
除非迫不得已,應儘量避免使用動態SQL,因為:
1、動態SQL難以除錯和故障診斷;
2、如果使用者向動態SQL提供了輸入,那麼可能存在SQL注入風險。
避免使用臨時表
1、除非卻有需要,否則應儘量避免使用臨時表,相反,可以使用表變數代替;
2、大多數時候(99%),表變數駐紮在記憶體中,因此速度比臨時表更快,臨時表駐紮在TempDb資料庫中,因此臨時表上的操作需要跨資料庫通訊,速度自然慢。
使用者定義函式中使用下列最佳實踐
不要在你的儲存過程,觸發器,函式和批處理中重複呼叫函式,例如,在許多時候,你需要獲得字串變數的長度,無論如
在儲存過程中使用下列最佳實踐
1、不要使用SP_xxx作為命名約定,它會導致額外的搜尋,增加I/O(因為系統儲存過程的名字就是以SP_開頭的),同時這麼做還會增加與系統儲存過程名稱衝突的機率;
2、將Nocount設定為On避免額外的網路開銷;
3、當索引結構發生變化時,在EXECUTE語句中(第一次)使用WITH RECOMPILE子句,以便儲存過程可以利用最新建立的索引;
4、使用預設的引數值更易於除錯。
在觸發器中使用下列最佳實踐
1、最好不要使用觸發器,觸發一個觸發器,執行一個觸發器事件本身就是一個耗費資源的過程;、
2、如果能夠使用約束實現的,儘量不要使用觸發器;
3、不要為不同的觸發事件(Insert,Update和Delete)使用相同的觸發器;
4、不要在觸發器中使用事務型程式碼。
在檢視中使用下列最佳實踐
1、為重新使用複雜的TSQL塊使用檢視,並開啟索引檢視;
2、如果你不想讓使用者意外修改表結構,使用檢視時加上SCHEMABINDING選項;
3、如果只從單個表中檢索資料,就不需要使用檢視了,如果在這種情況下使用檢視反倒會增加系統開銷,一般檢視會涉及多個表時才有用。
在事務中使用下列最佳實踐
1、SQL Server 2005之前,在BEGIN TRANSACTION之後,每個子查詢修改語句時,必須檢查@@ERROR的值,如果值不等於0,那麼最後的語句可能會導致一個錯誤,如果發生任何錯誤,事務必須回滾。從SQL Server 2005開始,Try..Catch..程式碼塊可以處理TSQL中的事務,因此在事務型程式碼中最好加上Try…Catch…;
2、避免使用巢狀事務,使用@@TRANCOUNT變數檢查事務是否需要啟動(為了避免巢狀事務);
3、儘可能晚啟動事務,提交和回滾事務要儘可能快,以減少資源鎖定時間。
要完全列舉最佳實踐不是本文的初衷,當你瞭解了這些技巧後就應該拿來使用,否則瞭解了也沒有價值。此外,你還需要評審和監視資料訪問程式碼是否遵循下列標準和最佳實踐。

資料庫引數配置

對MySQL配置優化 [在my.ini中配置最大併發數, 調整快取大小 ]
最重要的引數就是記憶體,我們主要用的innodb引擎,所以下面兩個引數調的很大

innodb_additional_mem_pool_size = 64M
innodb_buffer_pool_size = 1G

對於myisam,需要調整key_buffer_size
當然調整引數還是要看狀態,用show status語句可以看到當前狀態,以決定改調整哪些引數

port=3306
default-storage-engine=INNODB 
max_connections=100

恰當的硬體資源和作業系統

1、資料庫伺服器硬體升級,記憶體,硬碟等適當升級
2、記憶體超過4G的情況下,使用64位作業系統,建議使用Lunix作業系統,安裝MySQL 5.5及以上版本
3、讀寫[寫: update/delete/insert]分離
如果資料庫壓力很大,一臺機器無法支撐,那麼可以使用MySQL複製實現多臺機器同步,將資料庫的壓力分散。
Master、Slave1, Slave2...,其中Master用於寫,Slave1到n用於查,每個資料庫分擔的壓力小很多。
要實現這種方式,需要程式特別設計,寫都操作master,讀都操作slave,給程式開發帶來了額外負擔。當然目前已經有中介軟體來實現這個代理,對程 序來讀寫哪些資料庫是透明的。官方有個mysql-proxy,但是還是alpha版本的。新浪有個amobe for mysql,也可達到這個目的。
other:

[利用更多的資源]

1.1、客戶端多程序並行訪問

多程序並行訪問是指在客戶端建立多個程序(執行緒),每個程序建立一個與資料庫的連線,然後同時向資料庫提交訪問請求。當資料庫主機資源有空閒時,我們可以採用客戶端多程序並行訪問的方法來提高效能。如果資料庫主機已經很忙時,採用多程序並行訪問效能不會提高,反而可能會更慢。所以使用這種方式最好與DBA或系統管理員進行溝通後再決定是否採用。

例如:

我們有10000個產品ID,現在需要根據ID取出產品的詳細資訊,如果單執行緒訪問,按每個IO要5ms計算,忽略主機CPU運算及網路傳輸時間,我們需要50s才能完成任務。如果採用5個並行訪問,每個程序訪問2000個ID,那麼10s就有可能完成任務。

那是不是並行數越多越好呢,開1000個並行是否只要50ms就搞定,答案肯定是否定的,當並行數超過伺服器主機資源的上限時效能就不會再提高,如果再增加反而會增加主機的程序間排程成本和程序衝突機率。

以下是一些如何設定並行數的基本建議:

如果瓶頸在伺服器主機,但是主機還有空閒資源,那麼最大並行數取主機CPU核數和主機提供資料服務的磁碟數兩個引數中的最小值,同時要保證主機有資源做其它任務。

如果瓶頸在客戶端處理,但是客戶端還有空閒資源,那建議不要增加SQL的並行,而是用一個程序取回資料後在客戶端起多個程序處理即可,程序數根據客戶端CPU核數計算。

如果瓶頸在客戶端網路,那建議做資料壓縮或者增加多個客戶端,採用map reduce的架構處理。

如果瓶頸在伺服器網路,那需要增加伺服器的網路頻寬或者在服務端將資料壓縮後再處理了。

1.2、資料庫並行處理

資料庫並行處理是指客戶端一條SQL的請求,資料庫內部自動分解成多個程序並行處理,如下圖所示:

並不是所有的SQL都可以使用並行處理,一般只有對錶或索引進行全部訪問時才可以使用並行。資料庫表預設是不開啟並行訪問,所以需要指定SQL並行的提示,如下所示:

select /*+parallel(a,4)*/ * from employee;

並行的優點:

使用多程序處理,充分利用資料庫主機資源(CPU,IO),提高效能。

並行的缺點:

1、單個會話佔用大量資源,影響其它會話,所以只適合在主機負載低時期使用;

2、只能採用直接IO訪問,不能利用快取資料,所以執行前會觸發將髒快取資料寫入磁碟操作。

注:

1、並行處理在OLTP類系統中慎用,使用不當會導致一個會話把主機資源全部佔用,而正常事務得不到及時響應,所以一般只是用於資料倉庫平臺。

2、一般對於百萬級記錄以下的小表採用並行訪問效能並不能提高,反而可能會讓效能更差。

拓展

效能優化基於資料

資料備份與還原

create database test;
use test;
CREATE TABLE `new_table1` (
`id` int(11) NOT NULL,
`name` varchar(45) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `new_table1` VALUES (1, 'Jef'),(2, 'Ran'),(3, 'Da'), (4, 'Nan'),(5, 'Yuan'),(6, 'Gu');
CREATE TABLE `new_table2` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `new_table3` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

定時完成資料庫的備份
專案實際需求,請完成定時備份某個資料庫,或者定時備份資料庫的某些表的操作
windows 下每隔1小時,備份一次資料test
windows 每天晚上2:00 備份 test下 某一張表
cmd>

mysqldump –u root –p密碼 資料庫名 > 把資料庫放入到某個目錄

場景:備份 test庫的所有表
進入mysqldump所在的目錄C:\Program Files\MySQL\MySQL Server 5.7\bin,預設根目錄會自動配置到Path中,如果沒有也可以手動配置到Path中
場景:把test資料庫的所有表全部匯出備份
cmd>

mysqldump –u root –proot test > E:/saveMySQL/test.sql

場景:將test資料庫的new_table1 new_table2匯出備份
cmd>

mysqldump –u root –proot test new_table1 new_table2 > E:/saveMySQL/new_table1And2.sql 

如何恢復資料
進入的mysql操作介面
mysql>

source E:/saveMySQL/new_table1And2.sql

上述的字尾也可用.log的形式進行備份
定時備份:(把命令寫入到my.bat 中)
windows 如何定時備份 (每天凌晨2:00)
使用windows自帶的計劃任務,定時執行批處理命令。

增量備份和還原

定義:MySQL資料庫會以二進位制的形式,自動把使用者對mysql資料庫的操作,記錄到檔案,當用戶希望恢復的時候,可以使用備份檔案進行恢復。
增量備份會記錄dml語句、建立表的語句,不會記錄select。記錄的東西包括:sql語句本身、操作時間,位置
進行增量備份的步驟和恢復
注意:mysql5.0及之前的版本是不支援增量備份的
1、配置my.ini檔案或者my.conf,啟用二進位制備份。
開啟my.ini檔案,查詢log-bin,進行配置:

log-bin=E:/saveMySQL/MySQLSave.log

如果是5.7還要進行配置:

binlog_format=mixed

2、重啟MySQL服務
這個時候會在saveMySQL目錄下面看到以下兩個檔案:
MySQLSave.000001:日誌備份檔案。如果要檢視這個日誌檔案裡面的資訊,我們可以使用mysqlbinlog程式檢視,mysqlbinlog程式存放在MySQL的bin目錄下面,C:\Program Files\MySQL\MySQL Server 5.7\bin,預設根目錄會自動配置到Path中,如果沒有也可以手動配置到Path中
執行sql語句

update new_table1 set id = 20 where id=1;

Win + R,cmd

mysqlbinlog 備份檔案路徑E:\saveMySQL\MySqlSave.000001

MySqlSave.index:日誌索引檔案,裡面記錄了所以的日誌檔案。(E:\saveMySQL\MySqlSave.000001)
3、假設現在問題來了,我這條update是誤操作,如何進行恢復
在mysql日誌中會記錄每一次操作的時間和位置,所以我們既可以根據時間來恢復,也可以根據位置來恢復。
那麼,我們現在馬上可以從上圖看出,這條語句產生的時間是"180617 23:22:44",位置是1497
按時間來恢復
我們可以選擇在語句產生時間的前一秒
執行cmd命令:

mysqlbinlog --stop-datetime="2018-06-17 23:22:43" E:\saveMySQL\MySqlSave.000001 | mysql -uroot -p

這個時候我再執行SQL語句檢視
select * from new_table1;
結果變成了
按位置來恢復
執行cmd命令:

mysqlbinlog --stop-position="1497" E:\saveMySQL\MySqlSave.000001 | mysql -uroot -p

這個時候再執行SQL來檢視結果,又變回來了。



作者:傑克思勒
出處:http://www.cnblogs.com/tufujie/
如果您覺得閱讀本文對您有幫助,請點選一下右下方的推薦按鈕,您的推薦將是我寫作的最大動力!
版權宣告:本文為博主原創或轉載文章,歡迎轉載,但轉載文章之後必須在文章頁面明顯位置註明出處,否則保留追究法律責任的權利。