1. 程式人生 > 程式設計 >聊聊資料庫優化

聊聊資料庫優化

寫在前面的話

資料庫優化涉及到方方面面的知識,每種資料庫的架構,優化方式也都有著很大的差異,如果想做好資料庫優化要了解資料庫的技術架構、儲存結構、儲存方式、快取結構、SQL語句執行過程等有很深刻的瞭解。本文只是針對開發人員日常用到的通用的優化方法進行介紹,至於資料庫引數調整等資料庫相關內容也不再本文的討論範圍之內。目的是讓開發人員對常用資料庫優化有所瞭解。

資料庫優化的二八法則

二八法則認為,在任何一組東西中,最重要的只佔其中一小部分,約20%,其餘80%儘管是多數,卻是次要的,如:20%的富人擁有80%的財富。20%的權貴消耗了80%的資源,等等。資料庫的優化也符合二八法則:

  • 80%的效能問題是由20%的應用導致的。如少量大表的全表掃描導致的效能瓶頸。並不是應用一有問題,都需要對系統進行重構,只需優化少部分存在效能問題的功能便可以使系統的效能大幅度提升。
  • 80%的效能問題可以由20%的優化技術所解決。如增加索引,執行計劃分析等,能解決絕大部分效能問題

所以我們如果能夠瞭解常見的資料庫優化方法,可以解決開發中遇到的80%問題。

資料庫優化該怎麼做

在小品裡面問大象放冰箱分幾步?把冰箱門開啟,把大象放進去,把冰箱門關上。

資料庫優化分幾步?找到具體影響資料庫效能的原因,把問題解決了。

發現問題

誤區: 我的SQL語句執行的很快,問題肯定不會出在我這裡。

正解:SQL語句單次執行的快慢並不一定代表語句的好壞。有的語句雖然單次執行效率還比較高,但是隨著資料量的增加,併發量的增加很可能成為效能瓶頸。

該怎麼做?SQL寫好了之後看一下這條SQL的執行計劃,用事實說話。

那麼什麼是執行計劃呢?

SQL是一種傻瓜式語言,每一個條件就是一個需求,訪問的順序不同就形成了不同的執行計劃。資料庫必須做出選擇,一次只能有一種訪問路徑。執行計劃是一條查詢語句在資料庫中的執行過程或訪問路徑的描述。

每種資料庫都有工具可以檢視SQL的執行計劃。只有執行計劃沒問題才代表SQL沒問題。如果你能看懂了執行計劃。那麼恭喜你,優化問題你已經解決了80%。因為發現問題往往比解決問題更困難。

解決問題

上面說到了如何發現問題,其實除了看執行計劃還有其它手段,如檢視單個SQL執行次數等等,但是,執行計劃是開發人員最常用最直接的方式,能夠幫助我們解決大部分問題。那麼問題發現了之後如何解決呢?

簡單的說優化有兩個方向:能少做事儘量少做事,如果不能少做事儘量利用起伺服器的效能。

如何提高伺服器效能,可以利用併發的方式讓伺服器的資源儘量發揮到極致。有些資料庫本身就提供了一些併發機制,如oracle可以通過增加hint來設定SQL的併發數,也可以通過應用程式的併發來儘量壓榨出伺服器的效能。當然還可以通過增加伺服器,對資料庫進行分庫分表來提升效能。

另外一種方式是儘量少做事(敲黑板,劃重點,這很關鍵)。

如何讓資料庫少做事呢?方式也有很多,最常見的是通過索引。曾經有一位在ORACLE從業20餘年的專家說了這樣一句話,“其實我只懂一點IT知識,IT知識裡我只懂一點ORACLE,而ORACLE我也只懂一點資料庫,資料庫裡面只懂點SQL,SQL裡面只懂點索引”。可見索引對於資料庫優化有多麼重要。

聊聊索引

前面也提到索引對於資料庫的優化特別重要,接下來聊聊建立和使用索引時的一些注意事項。索引建立的字首性和可選擇性,索引建立的幾條建議,常見的索引被抑制情況。

索引的字首性

先看以下例子假設在員工表(emp)的(ENAME,JOB,MGR)三個欄位上建了一個索引,例如索引名叫IDX_1。三個欄位分別為員工姓名、工作和所屬經理號。然後,寫如下一個查詢語句,並不斷進行查詢條件和次序的排列組合,例如:

Select * from emp where ENAME=’a’ and JOB=’b’ and MGR=3;
Select * from emp where JOB=’b’ and MGR=3 and ENAME=’a’;
Select * from emp where JOB=’b’ and ENAME=’a’ and MGR=3;
Select * from emp where JOB=’b’ and MGR=3;
Select * from emp where ENAME=’a’ and MGR=3;
Select * from emp where ENAME=’a’;
Select * from emp where JOB=’b’;
Select * from emp where MGR=3;
複製程式碼

在各種條件組合情況下,剛才建的索引(IDX_1)是用還是不用?也就是說對emp表的訪問是全表掃描和還是按索引(IDX_1)訪問?

答案是隻要有ENAME=’a’條件,就能用上索引(IDX_1),而不是全表掃描。建立複合索引時一定要考慮到索引的字首性否則會由於沒有字首列在檢索條件中導致的全表掃描。

索引的字首性指的是必須用到索引的第一個欄位。

索引的可選擇性

索引的可選擇性,指的是不重複的索引值(基數)和表記錄數的比值。可選擇性是索引篩選能力的一個指標。當可選擇性越大,索引價值也就越大。

如一張訂單表order記錄為10萬條,表中user_id列的不重複值為10000,order_date列不重複值為1000,則建立在user_id上建立索引的查詢效率要比在order_date上建立索引的查詢效率高。這是因為,欄位值越多,可選性越強,按照索引查詢後需要定位的記錄越少,查詢效率越高。

幾條建立索引的建議

資料庫最常用的索引為B樹索引(不同的資料庫實現稍有不同,例如:oralce建立的是B*樹,mysql是B+樹),不同資料庫可能還有自己特有的索引如:oracle的點陣圖索引,mysql的hash索引等等。這裡我們只討論常用的B樹索引。下面給出幾條建立B樹索引時設計單欄位索引和複合索引的建議:

  1. 分析SQL語句中的約束條件欄位,如果約束條件欄位比較固定,則優先考慮建立針對多欄位複合索引。例如同時涉及到多個欄位的條件,則可以考慮建立一個複合索引。
  2. 如果單個欄位是主鍵或唯一欄位,或者可選性非常高的欄位,儘管約束條件欄位比較固定,也不一定要建成複合索引,可建成單欄位索引,降低複合索引開銷。
  3. 在複合索引設計中,需首先考慮複合索引第一個設計原則:複合索引的字首性。即SQL語句中,只有複合索引的第一個欄位作為約束條件,該複合索引才會啟用。
  4. 在複合索引設計中,其次應考慮複合索引的可選性。即按可選性高低,進行復合索引欄位的排序。
  5. 如果條件涉及的欄位不固定,組合比較靈活,則分別為不同的列建立單欄位索引。
  6. 如果是多表連線SQL語句,注意是否可以在被驅動表(drived table)的連線欄位與該表的其它約束條件欄位上,建立複合索引。
  7. 通過多種SQL分析工具,分析執行計劃並以量化形式評估效果。

常見的索引被抑制情況

在瞭解了索引建立的規則後根據業務需要建立好了索引,但是通過看語句的執行計劃發現仍然走的全表掃描,建立的索引沒有被使用。那麼索引不被使用都有可能因為什麼原因導致的呢。

1.索引列上有表示式或者函式操作。則索引是失效的。

如有如下語句:

select user_name from user where age -30 = 0

select user_name from user where age  = 30
複製程式碼

雖然age列上建立了索引,但是第一條語句依然是會按照全表掃描來執行的

2.存在隱式資料型別轉換

如有如下語句:

select user_name from user where age  = ‘30’
複製程式碼

但是user表定義的age列為number型別,在執行查詢時發生了隱式型別轉換。則索引被抑制。這種情況在開發過程中沒有上一種情況明顯。很容易被大家忽視。

3.資料可選性不高

例如user表有10萬條資料,但是性別列只有男女兩種,這種情況下,即使建立了索引,執行語句時也不會走索引。原因是根據索引查找出的結果集依然很大,查詢效率還不如全表掃描的效率高,這是資料庫就會執行全表掃描。

4.忽略的索引的字首性

如上文所述,執行語句時,忽略了索引的前置性,則執行語句時是不會走索引的。

使用is null或者is not null,null值並沒有被定義,所以索引會被抑制。

再聊一點

關於執行計劃

前面也提到SQL的優化很大程度上要依賴執行計劃,那麼執行計劃是如何生成出來的呢。簡單的說,資料庫會定期的收集資料庫中每個表的資料量等基礎資訊。當一條SQL傳送到資料庫要執行之前,在完成合法性檢查之後資料庫會根據每張表的資料量,索引等資訊通過計算給出一個SQL執行的最優計劃。

關於繫結變數

上文提到SQL的執行計劃需要通過各種資訊計算得到,那麼如果我把SQL的執行計劃快取起來。那麼每次當同一個SQL執行多次的時候不就免去了每次計算的代價嗎。實際上資料庫也正是這樣做的。資料庫會根據每條SQL的hash值將執行計劃快取到記憶體。

為了提高快取SQL的命中率,我們寫SQL的時候更多的使用佔位符。而不是直接傳值。

如有如下SQL:

select user_name from user where user_id = 1
select user_name from user where user_id = 2
複製程式碼

這時資料庫會當做兩條SQL來處理。因為快取並沒有命中。做如下修改:

select user_name from user where user_id = ?
複製程式碼

每次通過繫結不同的引數則會命中快取,減少了SQL的解析代價

那麼是不是所有的變數都要以繫結變數的方式來傳入呢?當然也不是。看下面的例子

如有訂單表t_order(user_id,order_date,amount),其中user_id為使用者ID,order_date為訂單日期

分別建立索引 :

idx_order_1(user_id)
idx_order_2(order_date)
複製程式碼

如果我希望查詢的SQL為:

select user_id,amount
from t_order
where user_id >= :num1
and user_id <= :num2
and order_date >= :date1
and order_date <= :date2
複製程式碼

這時當傳入的引數num1和num2之間範圍較小極限情況下num1=num2,也就是說我要查詢某一個人一段時間的訂單,則使用idx_order_2的效率會更高。而當傳入的引數num1和num2之間範圍較大但date1和date2的範圍較小時。如查詢某一天所有人的訂單,則使用idx_order_1的效率會更高。這時如果我們使用繫結變數的方式。由於資料庫分析執行計劃時並不能知道引數的範圍是什麼。也就不能夠給出最優的計劃,這時就不建議使用繫結變數方式來傳值。

本文也只是對資料庫常見的優化方式 進行了討論。要做好資料庫的優化還要在日常開發中多多嘗試。

作者介紹

李光明,民生科技有限公司,使用者體驗技術部Firefly移動金融開發平臺Java開發工程師。