接著上一篇 MySQL 資料庫效能最佳化之索引最佳化,這是 MySQL資料庫效能最佳化專題 系列的第四篇文章:MySQL 資料庫效能最佳化之SQL最佳化
有人反饋之前幾篇文章過於理論缺少實際操作細節,這篇文章就多一些可操作性的內容吧。
註:這篇文章是以 MySQL 為背景,很多內容同時適用於其他關係型資料庫,需要有一些索引知識為基礎
- 最佳化目標
- 減少 IO 次數
IO永遠是資料庫最容易瓶頸的地方,這是由資料庫的職責所決定的,大部分資料庫操作中超過90%的時間都是 IO 操作所佔用的,減少 IO 次數是 SQL 最佳化中需要第一優先考慮,當然,也是收效最明顯的最佳化手段。
- 降低 CPU 計算
除了 IO 瓶頸之外,SQL最佳化中需要考慮的就是 CPU 運算量的最佳化了。order by, group by,distinct … 都是消耗 CPU 的大戶(這些操作基本上都是 CPU 處理記憶體中的資料比較運算)。當我們的 IO 最佳化做到一定階段之後,降低 CPU 計算也就成為了我們 SQL 最佳化的重要目標
- 最佳化方法
- 改變 SQL 執行計畫
明確了最佳化目標之後,我們需要確定達到我們目標的方法。對於 SQL 陳述式來說,達到上述2個目標的方法其實只有一個,那就是改變 SQL 的執行計畫,讓他盡量“少走彎路”,盡量通過各種“捷徑”來找到我們需要的資料,以達到 “減少 IO 次數” 和 “降低 CPU 計算” 的目標
- 常見誤區
- count(1)和count(primary_key) 優於 count(*)
很多人為了統計記錄條數,就使用 count(1) 和 count(primary_key) 而不是 count(*) ,他們認為這樣效能更好,其實這是一個誤區。對於有些情境,這樣做可能效能會更差,應為資料庫對 count(*) 計數操作做了一些特別的最佳化。
- count(column) 和 count(*) 是一樣的
這個誤區甚至在很多的資深工程師或者是 DBA 中都普遍存在,很多人都會認為這是理所當然的。實際上,count(column) 和 count(*) 是一個完全不一樣的操作,所代表的意義也完全不一樣。
count(column) 是表示結果集中有多少個column欄位不為空白的記錄
count(*) 是表示整個結果集有多少條記錄
- select a,b from … 比 select a,b,c from … 可以讓資料庫訪問更少的資料量
這個誤區主要存在於大量的開發人員中,主要原因是對資料庫的儲存原理不是太瞭解。
實際上,大多數關係型資料庫都是按照行(row)的方式儲存,而資料存取操作都是以一個固定大小的IO單元(被稱作 block 或者 page)為單位,一般為4KB,8KB… 大多數時候,每個IO單元中儲存了多行,每行都是儲存了該行的所有欄位(lob等特殊類型欄位除外)。
所以,我們是取一個欄位還是多個欄位,實際上資料庫在表中需要訪問的資料量其實是一樣的。
當然,也有例外情況,那就是我們的這個查詢在索引中就可以完成,也就是說當只取 a,b兩個欄位的時候,不需要回表,而c這個欄位不在使用的索引中,需要回表取得其資料。在這樣的情況下,二者的IO量會有較大差異。
- order by 一定需要排序操作
我們知道索引資料實際上是有序的,如果我們的需要的資料和某個索引的順序一致,而且我們的查詢又通過這個索引來執行,那麼資料庫一般會省略排序操作,而直接將資料返回,因為資料庫知道資料已經滿足我們的排序需求了。
實際上,利用索引來最佳化有排序需求的 SQL,是一個非常重要的最佳化手段
延伸閱讀:MySQL ORDER BY 的實現分析 ,MySQL 中 GROUP BY 基本實現原理 以及 MySQL DISTINCT 的基本實現原理 這3篇文章中有更為深入的分析,尤其是第一篇
- 執行計畫中有 filesort 就會進行磁碟檔案排序
有這個誤區其實並不能怪我們,而是因為 MySQL 開發人員在用詞方面的問題。filesort 是我們在使用 explain 命令查看一條 SQL 的執行計畫的時候可能會看到在 “Extra” 一列顯示的資訊。
實際上,只要一條 SQL 陳述式需要進行排序操作,都會顯示“Using filesort”,這並不表示就會有檔案排序操作。
延伸閱讀:理解 MySQL Explain 命令輸出中的filesort,我在這裡有更為詳細的介紹
- 基本原則
- 盡量少 join
MySQL 的優勢在於簡單,但這在某些方面其實也是其劣勢。MySQL 最佳化器效率高,但是由於其統計資訊的量有限,最佳化器工作過程出現偏差的可能性也就更多。對於複雜的多表 Join,一方面由於其最佳化器受限,再者在 Join 這方面所下的功夫還不夠,所以效能表現離 Oracle 等關係型資料庫前輩還是有一定距離。但如果是簡單的單表查詢,這一差距就會極小甚至在有些情境下要優於這些資料庫前輩。
- 盡量少排序
排序操作會消耗較多的 CPU 資源,所以減少排序可以在快取命中率高等 IO 能力足夠的情境下會較大影響 SQL 的回應時間。
對於MySQL來說,減少排序有多種辦法,比如:
- 上面誤區中提到的通過利用索引來排序的方式進行最佳化
- 減少參與排序的記錄條數
- 非必要不對資料進行排序
- …
- 盡量避免 select *
很多人看到這一點後覺得比較難理解,上面不是在誤區中剛剛說 select 子句中欄位的多少並不會影響到讀取的資料嗎?
是的,大多數時候並不會影響到 IO 量,但是當我們還存在 order by 操作的時候,select 子句中的欄位多少會在很大程度上影響到我們的排序效率,這一點可以通過我之前一篇介紹 MySQL ORDER BY 的實現分析 的文章中有較為詳細的介紹。
此外,上面誤區中不是也說了,只是大多數時候是不會影響到 IO 量,當我們的查詢結果僅僅只需要在索引中就能找到的時候,還是會極大減少 IO 量的。
- 盡量用 join 代替子查詢
雖然 Join 效能並不佳,但是和 MySQL 的子查詢比起來還是有非常大的效能優勢。MySQL 的子查詢執行計畫一直存在較大的問題,雖然這個問題已經存在多年,但是到目前已經發布的所有穩定版本中都普遍存在,一直沒有太大改善。雖然官方也在很早就承認這一問題,並且承諾儘快解決,但是至少到目前為止我們還沒有看到哪一個版本較好的解決了這一問題。
- 盡量少 or
當 where 子句中存在多個條件以“或”並存的時候,MySQL 的最佳化器並沒有很好的解決其執行計畫最佳化問題,再加上 MySQL 特有的 SQL 與 Storage 分層架構方式,造成了其效能比較低下,很多時候使用 union all 或者是union(必要的時候)的方式來代替“or”會得到更好的效果。
- 盡量用 union all 代替 union
union 和 union all 的差異主要是前者需要將兩個(或者多個)結果集合并後再進行唯一性過濾操作,這就會涉及到排序,增加大量的 CPU 運算,加大資源消耗及延遲。所以當我們可以確認不可能出現重複結果集或者不在乎重複結果集的時候,盡量使用 union all 而不是 union。
- 盡量早過濾
這一最佳化策略其實最常見於索引的最佳化設計中(將過濾性更好的欄位放得更靠前)。
在 SQL 編寫中同樣可以使用這一原則來最佳化一些 Join 的 SQL。比如我們在多個表進行分頁資料查詢的時候,我們最好是能夠在一個表上先過濾好資料分好頁,然後再用分好頁的結果集與另外的表 Join,這樣可以儘可能多的減少不必要的 IO 操作,大大節省 IO 操作所消耗的時間。
- 避免類型轉換
這裡所說的“類型轉換”是指 where 子句中出現 column 欄位的類型和傳入的參數類型不一致的時候發生的類型轉換:
- 人為在column_name 上通過轉換函式進行轉換
直接導致 MySQL(實際上其他資料庫也會有同樣的問題)無法使用索引,如果非要轉換,應該在傳入的參數上進行轉換
- 由資料庫自己進行轉換
如果我們傳入的資料類型和欄位類型不一致,同時我們又沒有做任何類型轉換處理,MySQL 可能會自己對我們的資料進行類型轉換操作,也可能不進行處理而交由儲存引擎去處理,這樣一來,就會出現索引無法使用的情況而造成執行計畫問題。
- 優先最佳化高並發的 SQL,而不是執行頻率低某些“大”SQL
對於破壞性來說,高並發的 SQL 總是會比低頻率的來得大,因為高並發的 SQL 一旦出現問題,甚至不會給我們任何喘息的機會就會將系統壓跨。而對於一些雖然需要消耗大量 IO 而且響應很慢的 SQL,由於頻率低,即使遇到,最多就是讓整個系統響應慢一點,但至少可能撐一會兒,讓我們有緩衝的機會。
- 從全域出發最佳化,而不是片面調整
SQL 最佳化不能是單獨針對某一個進行,而應充分考慮系統中所有的 SQL,尤其是在通過調整索引最佳化 SQL 的執行計畫的時候,千萬不能顧此失彼,因小失大。
- 儘可能對每一條運行在資料庫中的SQL進行 explain
最佳化 SQL,需要做到心中有數,知道 SQL 的執行計畫才能判斷是否有最佳化餘地,才能判斷是否存在執行計畫問題。在對資料庫中啟動並執行 SQL 進行了一段時間的最佳化之後,很明顯的問題 SQL 可能已經很少了,大多都需要去發掘,這時候就需要進行大量的 explain 操作收集執行計畫,並判斷是否需要進行最佳化。