MySQL數據庫的Query優化

前言

由於Query語句對數據庫性能的影響非常大,因此優化查詢語句也是解決性能問題的重要一環。重點分析的MySQL Query Optimizer,其主要的功能就是通過計算分析系統中收集的各種統計信息,爲客戶端請求的Query 給出它認爲最優的執行計劃, 也就是他認爲最優的數據檢索方式。

當 MySQL Optimizer 接收到從Query Parser (解析器)送過來的 Query之後,會根據 MySQL Query語句的相應語法對該 Query 進行分解分析的同時,還會做很多其他的計算轉化工作。如常量轉化,無效內容刪除,常量計算等等。所有這些工作都只爲了Optimizer 工作的唯一目的,分析出最優的 數據檢索方式,也就是我們常說的執行計劃。

 

查詢語句優化基本思路和原則

在分析如何優化 MySQL Query 之前,我們需要先了解一下 Query 語句優化的基本思路和原則。一 般來說,Query 語句的優化思路和原則主要提現在以下幾個方面:

  1. 優化更需要優化的 Query;

  2. 定位優化對象的性能瓶頸;

  3. 明確的優化目標;

  4. 從 Explain 入手;

  5. 多使用 profile

  6. 永遠用小結果集驅動大的結果集;

  7. 儘可能在索引中完成排序;

  8. 只取出自己需要的 Columns;

  9. 僅僅使用最有效的過濾條件;

  10. 儘可能避免複雜的Join和子查詢;

前面4點可以理解爲 Query 優化的一個基本思路,後面部分則是我們優化中 的基本原則。

 

1、優化更需要優化的 Query

那什麼樣的 Query 是更需要優化呢?對於這個問題我們需要從對整個系統的影響來考慮。什麼 Query 的優化能給系統整體帶來更大的收益,就更需要優化。一般來說,高併發低消耗(相對)的 Query 對整個系統的影響遠比低併發高消耗的 Query 大。我們可以通過以下一個非常簡單的案例分析來 充分說明問題。

假設有一個 Query每小時執行10000 次,每次需要20個IO。另外一個Query每小時執行10次,每次需要20000個IO。

我們先通過 IO 消耗方面來分析。可以看出,兩個 Query 每小時所消耗的 IO 總數目是一樣的,都是 200000 IO/小時。假設我們優化第一個 Query,從20個IO降低到18個IO,也就是僅僅降低了 2 個IO, 則我們節省了 2 * 10000 = 20000 (IO/小時)。而如果希望通過優化第二個 Query 達到相同的效果, 我們必須要讓每個 Query 減少 20000 / 10 = 2000 IO。我想大家都會相信讓第一個 Query 節省 2 個 IO 遠比第二個 Query 節省 2000 個 IO 來的容易。

其次,如果通過 CPU 方面消耗的比較,原理和上面的完全一樣。只要讓第一個 Query 稍微節省一 小塊資源,就可以讓整個系統節省出一大塊資源,尤其是在排序,分組這些對CPU 消耗比較多的操作中 尤其突出。

最後,我們從對整個系統的影響來分析。一個頻繁執行的高併發Query 的危險性比一個低併發的 Query 要大很多。當一個低併發的 Query 走錯執行計劃,所帶來的影響主要只是該 Query 的請求者的 體驗會變差,對整體系統的影響並不會特別的突出,之少還屬於可控範圍。但是,如果我們一個高併發 的 Query 走錯了執行計劃,那所帶來的後可很可能就是災難性的,很多時候可能連自救的機會都不給你 就會讓整個系統 Crash 掉。曾經我就遇到這樣一個案例,系統中一個併發度較高的 Query 語句走錯執 行計劃,系統頃刻間 Crash,甚至我都還沒有反應過來是怎麼回事。當重新啓動數據庫提供服務後,系 統負載立刻直線飆升,甚至都來不及登錄數據庫查看當時有哪些 Active 的線程在執行哪些 Query。如 果是遇到一個併發並不太高的 Query 走錯執行計劃,至少我們還可以控制整個系統不至於系統被直接壓 跨,甚至連問題根源都難以抓到。

2、定位優化對象的性能瓶頸

當我們拿到一條需要優化的 Query 之後,第一件事情是什麼?是反問自己,這條 Query 有什麼問 題?我爲什麼要優化他?只有明白了這些問題,我們才知道我們需要做什麼,才能夠找到問題的關鍵。 而不能就只是覺得某個 Query 好像有點慢,需要優化一下,然後就開始一個一個優化方法去輪番嘗試。 這樣很可能整個優化過程會消耗大量的人力和時間成本,甚至可能到最後還是得不到一個好的優化結 果。這就像看病一樣,醫生必須要清楚的知道我們病的根源才能對症下藥。如果只是知道我們什麼地方 不舒服,然後就開始通過各種藥物嘗試治療,那這樣所帶來的後果可能就非常嚴重了。

所以,在拿到一條需要優化的 Query 之後,我們首先要判斷出這個 Query 的瓶頸到底是 IO 還是 CPU。到底是因爲在數據訪問消耗了太多的時間,還是在數據的運算(如分組排序等)方面花費了太多資 源?

一般來說,在 MySQL 5.0 系列版本中,我們可以通過系統自帶的 PROFILING 功能很清楚的找出一個 Query 的瓶頸所在。當然,如果讀者朋友爲了使用 MySQL 的某些在 5.1 版本中才有的新特性(如 Partition,EVENT等)亦或者是比較喜歡嘗試新事務而早早使用的MySQL 5.1 的預發佈版本,可能就沒 辦法使用這個功能了,因爲該功能在MySQL5.1 系列剛開始的版本中並不支持,不過讓人非常興奮的是該 功能在最新出來的 MySQL 5.1 正式版(5.1.30)又已經提供了。而如果讀者朋友正在使用的 MySQL 是 4.x 版本,那可能就只能通過自行分析 Query 的各個執行步驟,找到性能損失最大的地方。

 

3、明確的優化目標

當我們定爲到了一條 Query 的性能瓶頸之後,就需要通過分析該 Query 所完成的功能和 Query 對系統的整體影響制訂出一個明確的優化目標。沒有一個明確的目標,優化過程將是一個漫無目的而且低效的過程,也很難達收到一個理想的效果。尤其是對於一些實現應用中較爲重要功能點的Query 更是如此。

如何設定優化目標?這可能是很多人都非常頭疼的問題,對於我自己也一樣。要設定一個合理的優 化目標,不能過於理想也不能放任自由,確實是一件非常頭疼的事情。一般來說,我們首先需要清楚的 瞭解數據庫目前的整體狀態,同時也要清楚的知道數據庫中與該Query 相關的數據庫對象的各種信息, 而且還要了解該 Query 在整個應用系統中所實現的功能。瞭解了數據庫整體狀態,我們就能知道數據庫 所能承受的最大壓力,也就清楚了我們能夠接受的最悲觀情況。把握了該Query 相關數據庫對象的信 息,我們就應該知道實現該 Query 的消耗最理想情況下需要消耗多少資源,最糟糕又需要消耗多少資 源。最後,通過該 Query 所實現的功能點在整個應用系統中的重要地位,我們可以大概的分析出該 Query 可以佔用的系統資源比例,而且我們也能夠知道該 Query 的效率給客戶帶來的體驗影響到底有多 大。

當我們清楚了這些信息之後,我們基本可以得出該 Query 應該滿足的一個性能範圍是怎樣的,這也 就是我們的優化目標範圍,然後就是通過尋找相應的優化手段來解決問題了。如果該Query 實現的應用 系統功能比較重要,我們就必須讓目標更偏向於理想值一些,即使在其他某些方面作出一些讓步與犧 牲,比如調整 schema 設計,調整索引組成等,可能都是需要的。而如果該 Query 所實現的是一些並不 是太關鍵的功能,那我們可以讓目標更偏向悲觀值一些,而儘量保證其他更重要的Query 的性能。這種 時候,即使需要調整商業需求,減少功能實現,也不得不應該作出讓步。

 

4、從 Explain 入手

現在,優化目標也已經明確了,自然是奧開始動手的時候了。我們的優化到底該從何處入手呢?答 案只有一個,從Explain 開始入手。爲什麼?因爲只有Explain才能告訴你,這個 Query 在數據庫中是 以一個什麼樣的執行計劃來實現的。

但是,有一點我們必須清楚,Explain 只是用來獲取一個 Query 在當前狀態的數據庫中的執行計 劃,在優化動手之前,我們比需要根據優化目標在自己頭腦中有一個清晰的目標執行計劃。只有這樣, 優化的目標纔有意義。一個優秀的SQL 調優人員(或者成爲 SQL Performance Tuner),在優化任何一 個 SQL 語句之前,都應該在自己頭腦中已經先有一個預定的執行計劃,然後通過不斷的調整嘗試,再借 助 Explain 來驗證調整的結果是否滿足自己預定的執行計劃。對於不符合預期的執行計劃需要不斷分析 Query 的寫法和數據庫對象的信息,繼續調整嘗試,直至得到預期的結果。

當然,人無完人,並不一定每次自己預設的執行計劃都肯定是最優的,在不斷調整測試的過程中, 如果發現 MySQL Optimizer 所選擇的執行計劃的實際執行效果確實比自己預設的要好,我們當然還是應 該選擇使用 MySQL optimizer 所生成的執行計劃。

上面的這個優化思路,只是給大家指了一個優化的基本方向,實際操作還需要讀者朋友不斷的結合 具體應用場景不斷的測試實踐來體會。當然也並不一定所有的情況都非要嚴格遵循這樣一個思路,規則 是死的,人是活的,只有更合理的方法,沒有最合理的規則。

 

5、永遠用小結果集驅動大的結果集

很多人喜歡在優化 SQL 的時候說用小表驅動大表,個人認爲這樣的說法不太嚴謹。爲什麼?因 爲大表經過 WHERE 條件過濾之後所返回的結果集並不一定就比小表所返回的結果集大,可能反而更小(where之後的小表)。 在這種情況下如果仍然採用小表驅動大表,就會得到相反的性能效果。

其實這樣的結果也非常容易理解,在MySQL 中的 Join,只有 Nested Loop 一種 Join 方式,也就是 MySQL 的 Join 都是通過嵌套循環來實現的。驅動結果集越大,所需要循環的此時就越多,那麼被驅動表 的訪問次數自然也就越多,而每次訪問被驅動表,即使需要的邏輯IO 很少,循環次數多了,總量自然也 不可能很小,而且每次循環都不能避免的需要消耗 CPU ,所以 CPU 運算量也會跟着增加。所以,如果 我們僅僅以表的大小來作爲驅動表的判斷依據,假若小表過濾後所剩下的結果集比大表多很多,結果就 是需要的嵌套循環中帶來更多的循環次數,反之,所需要的循環次數就會更少,總體IO 量和 CPU 運算 量也會少。而且,就算是非 Nested Loop 的 Join 算法,如 Oracle 中的 Hash Join,同樣是小結果集 驅動大的結果集是最優的選擇。

所以,在優化 Join Query 的時候,最基本的原則就是“小結果集驅動大結果集”,通過這個原則 來減少嵌套循環中的循環次數,達到減少IO 總量以及 CPU 運算的次數。
儘可能在索引中完成排序

6、只取出自己需要的 Columns

如果是需要排序的 Query 來說,影響就更大了。在 MySQL 中存在兩種排序算法,一種是在 MySQL4.1 之前的老算法,實現方式是先將需要排序的字段和可以直接定位到相關行數據的指針信息取 出,然後在我們所設定的排序區(通過參數sort_buffer_size 設定)中進行排序,完成排序之後再次 通過行指針信息取出所需要的 Columns,也就是說這種算法需要訪問兩次數據。第二種排序算法是從 MySQL4.1 版本開始使用的改進算法,一次性將所需要的Columns 全部取出,在排序區中進行排序後直 接將數據返回給請求客戶端。改行算法只需要訪問一次數據,減少了大量的隨機IO,極大的提高了帶有 排序的 Query 語句的效率。但是,這種改進後的排序算法需要一次性取出並緩存的數據比第一種算法 要多很多,如果我們將並不需要的Columns 也取出來,就會極大的浪費排序過程所需要的內存。在 MySQL4.1 之後的版本中,我們可以通過設置 max_length_for_sort_data 參數大小來控制 MySQL 選擇 第一種排序算法還是第二種排序算法。當所取出的Columns 的單條記錄總大小 max_length_for_sort_data 設置的大小的時候,MySQL 就會選擇使用第一種排序算法,反之,則會選 擇第二種優化後的算法。爲了儘可能提高排序性能,我們自然是更希望使用第二種排序算法,所以在 Query 中僅僅取出我們所需要的 Columns 是非常有必要的。

 

7、僅僅使用最有效的過濾條件

很多人在優化 Query 語句的時候很容易進入一個誤區,那就是覺得 WHERE 子句中的過濾條件越多 越好,實際上這並不是一個非常正確的選擇。其實我們分析Query 語句的性能優劣最關鍵的就是要讓他選擇一條最佳的數據訪問路徑,如何做到通過訪問最少的數據量完成自己的任務。爲什麼說過濾條件多不一定是好事呢?請看下面示例:

需求: 查找某個用戶在所有 group 中所發的討論 message 基本信息。 場景:

1、知道用戶 ID 和用戶 nick_name

2、信息所在表爲 group_message
3、group_message 中存在用戶 ID(user_id)和 nick_name(author)兩個索引

方案一:將用戶 ID 和用戶 nick_name 兩者都作爲過濾條件放在WHERE子句中來查詢,Query 的執行計 劃如下:

方案二:僅僅將用戶 ID 作爲過濾條件放在 WHERE 子句中來查詢,Query 的執行計劃如下:

方案三:僅將用戶 nick_name 作爲過濾條件放在 WHERE 子句中來查詢,Query 的執行計劃如下:

初略一看三個執行計劃好像都挺好的啊,每一個 Query 的執行類型都利用到了索引,而且都是 “ref”類型。可是仔細一分析,就會發現,group_message_uid_ind 索引的索引鍵長度爲4(key_len: 4),由於 user_id 字段類型爲 int,所以我們可以判定出 Query Optimizer 給出的這個索引鍵長度是 完全準確的。而 group_message_author_ind 索引的索引鍵長度爲 98(key_len: 98),因爲 author 字 段定義爲 varchar(32) ,而所使用的字符集是 utf8,32 * 3 + 2 = 98。而且,由於 user_id 與 author(來源於 nick_name)全部都是一一對應的,所以同一個 user_id 有哪些記錄,那麼所對應的 author 也會有完全相同的記錄。所以,同樣的數據在 group_message_author_ind 索引中所佔用的存儲 空間要遠遠大於 group_message_uid_ind 索引所佔用的空間。佔用空間更大,代表我們訪問該索引所需 要讀取的數據量就會更多。所以,選擇 group_message_uid_ind 的執行計劃纔是最有的執行計劃。也就 是說,上面的方案二纔是最有方案,而使用了更多的 WHERE 條件的方案一反而沒有僅僅使用 user_id 一個過濾條件的方案一優。

可能有些人會說,那如果將 user_id 和 author 兩者建立聯合索引呢?告訴你,效果可能比沒有這 個索引的時候更差,因爲這個聯合索引的索引鍵更長,索引佔用的空間將會更大。

這個示例並不一定能代表所有場景,僅僅是希望讓大家明白,並不是任何時候都是使用的過濾條件 越多性能會越好。在實際應用場景中,肯定會存在更多更復雜的情形,怎樣使我們的Query 有一個更優 化的執行計劃,更高效的性能,還需要靠大家仔細分析各種執行計劃的具體差別,才能選擇出更優化的 Query。

8、儘可能避免複雜的 Join 和子查詢

我們都知道,MySQL 在併發這一塊做的並不是太好,當併發量太高的時候,系統整體性能可能會急 劇下降,尤其是遇到一些較爲複雜的 Query 的時候更是如此。這主要與 MySQL 內部資源的爭用鎖定控 制有關,如讀寫相斥等等。對於 Innodb 存儲引擎由於實現了行級鎖定可能還要稍微好一些,如果使用 的 MyISAM 存儲引擎,併發一旦較高的時候,性能下降非常明顯。所以,我們的Query 語句所涉及到的 表越多,所需要鎖定的資源就越多。也就是說,越複雜的Join 語句,所需要鎖定的資源也就越多,所 阻塞的其他線程也就越多。相反,如果我們將比較複雜的 Query 語句分拆成多個較爲簡單的 Query語

句分步執行,每次鎖定的資源也就會少很多,所阻塞的其他線程也要少一些。

可能很多讀者會有疑問,將複雜 Join 語句分拆成多個簡單的 Query 語句之後,那不是我們的網絡 交互就會更多了嗎?網絡延時方面的總體消耗也就更大了啊,完成整個查詢的時間不是反而更長了嗎? 是的,這種情況是可能存在,但也並不是肯定就會如此。我們可以再分析一下,一個複雜的Join Query 語句在執行的時候,所需要鎖定的資源比較多,可能被別人阻塞的概率也就更大,如果是一個簡單的 Query,由於需要鎖定的資源較少,被阻塞的概率也會小很多。所以較爲複雜的 Join Query 也有可能 在執行之前被阻塞而浪費更多的時間。而且,我們的數據庫所服務的並不是單單這一個Query 請求,還 有很多很多其他的請求,在高併發的系統中,犧牲單個 Query 的短暫響應時間而提高整體處理能力也是 非常值得的。優化本身就是一門平衡與取捨的藝術,只有懂得取捨,平衡整體,才能讓系統更優。

對於子查詢,可能不需要我多說很多人就明白爲什麼會不被推薦使用。在MySQL 中,子查詢的實現 目前還比較差,很難得到一個很好的執行計劃,很多時候明明有索引可以利用,可Query Optimizer 就 是不用。從 MySQL 官方給出的信息說,這一問題將在 MySQL6.0 中得到較好的解決,將會引入 SemiJoin 的執行計劃,可 MySQL6.0 離我們投入生產環境使用恐怕還有很遙遠的一段時間。所以,在 Query 優化的過程中,能不用子查詢的時候就儘量不要使用子查詢。

上面這些僅僅只是一些常用的優化原則,並不是說在 Query 優化中就只需要做到這些原則就可以, 更不是說 Query 優化只能通過這些原則來優化。在實際優化過程中,我們還可能會遇到很多帶有較爲復 雜商業邏輯的場景,具體的優化方法就只能根據不同的應用場景來具體分析,逐步調整。其實,最有效的優化,就是不要用,也就是不要實現這個商業需求。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章