想做到數據庫優化的高手,不是花幾周,幾個月就能達到的,這並不是因為數據庫優化有多高深,而是因為要做好優化一方面需要有非常好的技術功底,對操作系統、存儲硬件網絡、數據庫原理等方面有比較扎實的基礎知識,另一方面是需要花大量時間對特定的數據庫不斷的進行實踐測試與總結。
針對數據庫的優化,可以已Oracle為基點,從Oracle外部因素和Oracle本身的性能兩部分考慮。
一、Oracle的外部因素:
Oracle的外部因素包括CUP,cache L1,L2,L3, 內存,網卡,普通硬盤/SSD硬盤。 這些硬件基於生產條件的限制,也就存在了相應的數據處理的瓶頸。
下面是外部因素,對數據I/O處理的性能指標:

通過圖片可以看出來,有兩個指標:
延時(響應時間):表示硬件處理突發事件的反應能力。
帶寬(吞吐量):代表硬件的持續處理能力。
根據數據庫知識,我們可以列出每種硬件主要的工作內容:
CPU及內存:緩存數據訪問、比較、排序、事務檢測、SQL解析、函數或邏輯運算;
網絡:結果數據傳輸、SQL請求、遠程數據庫訪問(dblink);
硬盤/SSD硬盤:數據訪問、數據寫入、日志記錄、大數據量排序、大表連接。
從上圖可以看出,計算機系統硬件性能從高到代依次為:
CPU——Cache(L1-L2-L3)——內存——SSD硬盤——網絡——硬盤
可見SSD硬盤性能的平均值遠遠高於普通硬盤。值得一提的是,阿里辦公環境全部為MAC系統,而MAC系統就是SSD硬盤。
下面給出優化建議:
從實際意義上來講,根據需求,數據訪問的需求只會越來越大,通過SQL語句減少訪問量的做法,對大數據而言,效果提升也是有限的
對於Oracle以外的因素,可以從兩點進行考慮,第一就是更換服務器的硬件設施,將普通硬盤換成SSD固態硬盤,第二點也就是基於分布式的思想,增加多台計算機充當服務器對數據進行處理。
二、從Oracle內部的自身性能進行優化
從Oracle內部自身考慮的話,可以從如下5個方向考慮:
1、減少數據訪問 ---- 索引的使用 (減少磁盤訪問)
2、返回更少的數據 ----分頁的應用 (減少網絡傳輸或磁盤訪問)
3、減少交互次數 ---- 減少I/O的訪問,邏輯上的優化 (減少網絡傳輸)
4、減少數據庫服務器CPU運算 ----增加客戶端的運算 (減少CPU及內存開銷)
5、利用更多的資源 ---- 分布式的嘗試 (增加資源)
1、減少數據訪問
1.1、正確認識索引,創建索引
索引會大大增加表記錄的DML(INSERT,UPDATE,DELETE)開銷,正確的索引可以讓性能提升100,1000倍以上,不合理的索引也可能會讓性能下降100倍,因此在一個表中創建什么樣的索引需要平衡各種業務需求。
如果查詢需求特別巨大,同時滿足一次插入,偶爾修改,不刪除,這樣的情況下可以充分考慮增加數據庫索引,來提高查詢性能
1.2、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(兩個表通過索引字段關聯)
1.3、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 |
給索引查詢的值應是已知數據,不能是未知字段值。 |
有時候我們會使用多個字段的組合索引,如果查詢條件中第一個字段不能使用索引,那整個查詢也不能使用索引
如:我們company表建了一個id+name的組合索引,以下SQL是不能使用索引的
Select * from company where name=?
因為ID一般都會被設為主鍵,而主鍵已經起到了唯一約束的作用,不需要再設為索引。
1.4、我們一般在什么字段上建索引?
這是一個非常復雜的話題,需要對業務及數據充分分析后再能得出結果。主鍵及外鍵通常都要有索引,其它需要建索引的字段應滿足以下條件:
1、字段出現在查詢條件中,並且查詢條件可以使用索引;
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 |
1.5、如何知道SQL是否使用了正確的索引?
簡單SQL可以根據索引使用語法規則判斷,復雜的SQL不好辦,判斷SQL的響應時間是一種策略,但是這會受到數據量、主機負載及緩存等因素的影響,有時數據全在緩存里,可 能全表訪問的時間比索引訪問時間還少。要准確知道索引是否正確使用,需要到數據庫中查看SQL真實的執行計划,這個話題比較復雜,詳見SQL執行計划專題介紹。
1.6、索引對DML(INSERT,UPDATE,DELETE)附加的開銷有多少?
這個沒有固定的比例,與每個表記錄的大小及索引字段大小密切相關,以下是一個普通表測試數據,僅供參考:
索引對於Insert性能降低56%
索引對於Update性能降低47%
索引對於Delete性能降低29%
因此對於寫IO壓力比較大的系統,表的索引需要仔細評估必要性,另外索引也會占用一定的存儲空間。
如果不涉及增刪改,可以考慮使用索引
1.7、如何創建索引
有些時候,我們只是訪問表中的幾個字段,並且字段內容較少,我們可以為這幾個字段單獨建立一個組合索引,這樣就可以直接只通過訪問索引就能得到數據,一般索引占用的 磁盤空間比表小很多,所以這種方式可以大大減少磁盤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表。
2、返回更少的數據
一般指的是分頁技術
2.1、客戶端(應用程序或瀏覽器)分頁 -----缺點大於優點,不建議使用
將數據從應用服務器全部下載到本地應用程序或瀏覽器,在應用程序或瀏覽器內部通過本地代碼進行分頁處理
優點:編碼簡單,減少客戶端與應用服務器網絡交互次數
缺點:首次交互時間長,占用客戶端內存
適應場景:客戶端與應用服務器網絡延時較大,但要求后續操作流暢,如手機GPRS,超遠程訪問(跨國)等等。
2.2、應用服務器分頁 --------- 很少有數據庫系統不支持分頁,但是在服務器中進行分頁的效率與在Oracle中進行分頁的效率對比,目前還未知
將數據從數據庫服務器全部下載到應用服務器,在應用服務器內部再進行數據篩選。以下是一個應用服務器端Java程序分頁的示例:
List list=executeQuery(“select * from employee order by id”);
Int count= list.size();
List subList= list.subList(10, 20);
優點:編碼簡單,只需要一次SQL交互,總數據與分頁數據差不多時性能較好。
缺點:總數據量較多時性能較差。
適應場景:數據庫系統不支持分頁處理,數據量較小並且可控。
2.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);
3、減少交互次數
數據的增刪改查四個操作中,插入和查找普遍存在大批量的操作,較少交互次數也是主要針對這兩個操作。
但是,一般項目中都是存在一次插入多次查詢,甚至千萬次查詢,所以,這里還是主要考慮減少查詢的交互次數。
3.1 使用 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請求的數量,從而提高性能。在優化中 n個ID放到一個List中,這樣查詢的交互數量只有1。
那如果有10000個ID,那是不是全部放在一條SQL里處理呢?答案肯定是否定的。首先大部份數據庫都會有SQL長度和IN里個數的限制,如ORACLE的IN里就不允許超過1000個值。
另外當前數據庫一般都是采用基於成本的優化規則,當IN數量達到一定值時有可能改變SQL執行計划,從索引訪問變成全表訪問,這將使性能急劇變化。隨着SQL中IN的里面的值個數增加,SQL的執行計划會更復雜,占用的內存將會變大,這將會增加服務器CPU及內存成本。
評估在IN里面一次放多少個值還需要考慮應用服務器本地內存的開銷,有並發訪問時要計算本地數據使用周期內的並發上限,否則可能會導致內存溢出。
綜合考慮,一般IN里面的值個數超過20個以后性能基本沒什么太大變化,也特別說明不要超過100,超過后可能會引起執行計划的不穩定性及增加數據庫CPU及內存成本。
3.2、設置Fetch Size
當我們采用select從數據庫查詢數據時,數據默認並不是一條一條返回給客戶端的,也不是一次全部返回客戶端的,而是根據客戶端fetch_size參數處理,每次只返回fetch_size條記錄,當客戶端游標遍歷到尾部時再從服務端取數據,直到最后全部傳送完成。所以如果我們要從服務端一次取大量數據時,可以加大fetch_size,這樣可以減少結果數據傳輸的交互次數及服務器數據准備時間,提高性能。
3.3、使用存儲過程
大型數據庫一般都支持存儲過程,合理的利用存儲過程也可以提高系統性能。如你有一個業務需要將A表的數據做一些加工然后更新到B表中,但是又不可能一條SQL完成,這時你需要如下3步操作:
a:將A表數據全部取出到客戶端;
b:計算出要更新的數據;
c:將計算結果更新到B表。
如果采用存儲過程你可以將整個業務邏輯封裝在存儲過程里,然后在客戶端直接調用存儲過程處理,這樣可以減少網絡交互的成本。
當然,存儲過程也並不是十全十美,存儲過程有以下缺點:
a、不可移植性,每種數據庫的內部編程語法都不太相同,當你的系統需要兼容多種數據庫時最好不要用存儲過程。
b、學習成本高,DBA一般都擅長寫存儲過程,但並不是每個程序員都能寫好存儲過程,除非你的團隊有較多的開發人員熟悉寫存儲過程,否則后期系統維護會產生問題。
c、業務邏輯多處存在,采用存儲過程后也就意味着你的系統有一些業務邏輯不是在應用程序里處理,這種架構會增加一些系統維護和調試成本。
d、存儲過程和常用應用程序語言不一樣,它支持的函數及語法有可能不能滿足需求,有些邏輯就只能通過應用程序處理。
e、如果存儲過程中有復雜運算的話,會增加一些數據庫服務端的處理成本,對於集中式數據庫可能會導致系統可擴展性問題。
f、為了提高性能,數據庫會把存儲過程代碼編譯成中間運行代碼(類似於java的class文件),所以更像靜態語言。當存儲過程引用的對像(表、視圖等等)結構改變后,存儲過程需要重新編譯才能生效,在24*7高並發應用場景,一般都是在線變更結構的,所以在變更的瞬間要同時編譯存儲過程,這可能會導致數據庫瞬間壓力上升引起故障(Oracle數據庫就存在這樣的問題)。
個人觀點:普通業務邏輯盡量不要使用存儲過程,定時性的ETL任務或報表統計函數可以根據團隊資源情況采用存儲過程處理。
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支持最大並發數 |
磁盤IO支持最大並發數 |
| 不使用 |
2*4*1000=8000 |
100*160=16000 |
| 使用 |
2*4*1000/0.02=400000 |
100*160=16000 |
從以上計算可以看出,不使用綁定變量的系統當並發達到8000時會在CPU上產生瓶頸,當使用綁定變量的系統當並行達到16000時會在磁盤IO上產生瓶頸。所以如果你的系統CPU有瓶頸時請先檢查是否存在大量的硬解析操作。
4.2、減少比較操作
我們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方法可以在網上直接查詢,這里不詳細介紹。
4.3、大量復雜運算在客戶端進行
什么是復雜運算,一般認為是一秒鍾CPU只能做10萬次以內的運算。如含小數的對數及指數運算、三角函數、3DES及BASE64數據加密算法等等。
如果有大量這類函數運算,盡量放在客戶端處理,一般CPU每秒中也只能處理1萬-10萬次這樣的函數運算,放在數據庫內不利於高並發處理
5、利用更多資源
5、客戶端多進程訪問 -----通常意義上的分布式
多進程並行訪問是指在客戶端創建多個進程(線程),每個進程建立一個與數據庫的連接,然后同時向數據庫提交訪問請求。當數據庫主機資源有空閑時,我們可以采用客戶端多進程並行訪問的方法來提高性能。如果數據庫主機已經很忙時,采用多進程並行訪問性能不會提高,反而可能會更慢。
例如:
我們有10000個產品ID,現在需要根據ID取出產品的詳細信息,如果單線程訪問,按每個IO要5ms計算,忽略主機CPU運算及網絡傳輸時間,我們需要50s才能完成任務。如果采用5個並行訪問,每個進程訪問2000個ID,那么10s就有可能完成任務。
以下是一些如何設置並行數的基本建議:
如果瓶頸在服務器主機,但是主機還有空閑資源,那么最大並行數取主機CPU核數和主機提供數據服務的磁盤數兩個參數中的最小值,同時要保證主機有資源做其它任務。
如果瓶頸在客戶端處理,但是客戶端還有空閑資源,那建議不要增加SQL的並行,而是用一個進程取回數據后在客戶端起多個進程處理即可,進程數根據客戶端CPU核數計算。
如果瓶頸在客戶端網絡,那建議做數據壓縮或者增加多個客戶端,采用map reduce的架構處理。
如果瓶頸在服務器網絡,那需要增加服務器的網絡帶寬或者在服務端將數據壓縮后再處理了。
總結:
1、減少數據訪問 ---- 索引的使用
2、返回更少的數據 ----分頁的應用、也是減少I/O的訪問
3、減少交互次數 ---- 減少I/O的訪問,邏輯上的優化
4、減少數據庫服務器CPU運算 ----增加客戶端的運算
5、利用更多的資源 ---- 分布式的嘗試
6、主體思想保證服務器到不了並發瓶頸,在這個前提下,嘗試優化
7、常用的優化策略首選索引,其次是交互次數和分頁技術的使用
8、在數據量極其龐大的前提下,首選分布式處理方式
