MySQL系列-- 4. 查詢性能優化

4. 查詢性能優化

4.1 爲何查詢速度會變慢

  • 快速查詢取決於響應時間
  • 若是把查詢當作是一個任務,那麼它由一系列子任務組成,每一個子任務都會消耗必定的時間。若是要優化查詢,實際上要優化其子任務,要麼消除其中一些子任務,要麼減小子任務的執行次數,要麼讓子任務運行得更快(有時候須要修改一些查詢,減小這些查詢對系統中運行的其餘查詢的影響,這時候是在減小一個查詢的資源消耗)。
  • 查詢的生命週期:
    • 從客戶端
    • 到服務器
    • 在服務器上:
      • 解析
      • 生成執行計劃
      • 執行:最重要的階段。包括了大量爲了檢索數據到存儲引擎的調用及調用後的數據處理,包括排序,分組等。
    • 返回結果給客戶端
  • 完成這些任務,須要在不一樣的地方花費時間。包括網絡,CPU計算,生成統計信息和執行計劃、鎖等待(互斥等待)等操做,尤爲是向底層存儲引擎檢索數據的調用操做,這些調用須要在內存操做、CPU操做和內存不足時致使的IO操做上消耗的時間。根據存儲引擎的不一樣,可能還會產生大量的上下文切換以及系統調用。

4.2 慢查詢基礎:優化數據訪問:

查詢性能低下最根本的緣由是訪問的數據太多。某些查詢可能不可避免地須要篩選大量數據,但這並不常見。大部分性能低下的查詢均可以經過減小訪問的數據量進行優化。對於低效的查詢,能夠經過下面兩個步驟分析:mysql

  • 確認應用程序是否在檢索大量超過須要的數據。這一般意味着訪問了太多的行,但有時候也多是訪問了太多的列。
  • 確認MySQL服務器層是否在分析大量超過須要的數據行。

4.2.1 是否向數據庫請求了不須要的數據

有些查詢會請求超過實際須要的數據,而後這些多餘的數據會被應用程序丟棄。這會給MySQL服務器帶來額外的負擔,並增長網絡開銷,另外也會消耗應用服務器的CPU和內存資源。算法

典型案例:sql

  • 查詢不須要的記錄:一個常見錯誤是誤覺得MySQL只會返回須要的數據,實際上MySQL確實先返回所有結果集再進行計算。最簡單有效的辦法是在這樣的查詢後面加上LIMIT。
  • 多表關聯時返回所有列:正確作法是隻取須要的列
  • 老是取出所有的列:
    • 取出所有的列,會讓優化其沒法完成覆蓋掃描這類優化,還會給服務器帶來額外的資源消耗。要慎重。
    • 可是這能夠簡化開發,提供相同代碼片斷的複用性,或者應用程序使用了某種緩存機制等其餘有必要取出所有列的因素。若是清楚這麼作的性能影響,也是能夠考慮的。
  • 重複查詢相同的數據:建議初次查詢的時候將這個數據緩存起來,須要的時候從緩存中取出。

4.2.2 MySQL是否在掃描額外的記錄

對於MySQL,最簡單的衡量查詢開銷的三個指標:響應時間、掃描的行數和返回的行數。沒有哪一個指標可以完美地衡量查詢的開銷,但它們大體反映了MySQL在內部執行查詢時須要訪問多少數據,並能夠大概推算出查詢運行的時間。這三個指標都會記錄到MySQL的慢日誌中,檢查慢日誌記錄是找出掃描行數過多的查詢的好辦法。數據庫

  • 響應時間:
    • 響應時間=服務時間+排隊時間。實際上沒辦法細分,目前尚未辦法測量。
      • 服務時間:數據庫處理這個查詢真正花的時間。
      • 排隊時間:服務器由於等待某些資源而沒有真正執行查詢的時間。
    • 看到一個查詢的響應時間的時候,要評估是否合理。歸納地說,瞭解這個查詢須要那些索引以及它的執行計劃是什麼,而後計算大概須要多少個順序和隨機IO,在用其乘以在具體硬件條件下一次IO消耗的時間,最後把這些消耗都加起來獲得一個參考值。
  • 掃描的行數和返回的行數
    • 分析查詢時,查看該查詢掃描的行數是很是有幫助的。在必定程度上可以說明該查詢找到須要的數據效率高不高。
    • 不過這個指標還不夠完美,由於並非全部的行的訪問代價都是同樣的。
    • 理想狀況下掃描的行數和返回的行數應該是相同的。但實際狀況下不多存在,好比關聯查詢。通常掃描的行數與返回的行數比率一般在1:1和10:1之間。
  • 掃描的行數和訪問類型
    • 在評估查詢開銷的時候,須要考慮從表中找到某一行數據的成本。
    • 訪問類型(EXPLAIN語句中的type列,row列顯示掃描的行數):
      • ref
      • ALL(全表掃描)
    • MySQL應用WHERE條件的方式:
      • 在索引中使用WHERE條件來過濾不匹配的記錄,這是在存儲引擎完成的。
      • 使用覆蓋索引掃描(在EXTRA 列中出現了Using index)來返回記錄,直接從索引中過濾不須要的記錄並返回命中結果。這是在MySQL服務器完成的,但無須再回表查詢
      • 從數據表中返回數據,而後過濾不知足條件的記錄(在Extra列中出現Using Where)。這在MySQL服務器層完成,MySQL須要先從數據表讀出記錄而後過濾。
    • 優化技巧:
      • 使用索引覆蓋掃描,把全部須要的列都放到索引中,這樣存儲引擎無須回表獲取對應的行就能夠返回結果。
      • 改變庫表結構。例如使用單獨的彙總表
      • 重寫這個複雜的查詢,讓MySQL優化器可以以更優化的方式執行這個查詢。

4.3 重構查詢方式

4.3.1 一個複雜的查詢仍是多個簡單的查詢

  • MySQL從設計上讓鏈接和斷開鏈接都很輕量級,在返回一個小的查詢結果方面很高效。
  • MySQL內部每秒可以掃描內存中上百萬行數據,相比之下,MySQL響應數據給客戶端就慢得多了。
  • 使用盡量少的查詢,但某些場景下將一個大查詢分解爲多個小查詢是頗有必要的。

4.3.2 切分查詢

  • 有時候須要對一個大查詢切分紅小查詢,每一個查詢功能徹底同樣,只完成一小部分,每次只返回一小部分查詢結果。
  • 例如刪除舊的數據,切分該查詢能夠儘量小地影響性能,還能夠減小MySQL複製的延遲。一次刪除一萬行數據通常來講是一個比較高效並且對服務器影響也最小的作法,若是是事務型引擎,不少時候小事務可以更高效。另外,每次刪除後都暫停一會,可以將一次性的壓力分散到一個很長的時間段中,就能夠大大下降對服務器的影響,還能夠大大減小刪除時鎖的持有時間。

4.3.3 分解關聯查詢

不少高性能的應用都會對關聯查詢進行分解。簡單地,能夠對每個表進行一次單表查詢,而後將結果在應用程序中進行管理緩存

SELECT * FROM tag
    JOIN tag_post ON tag_post.tag_id=tag.id
    JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
-- 能夠分解成:
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post where tag_id=1234;
SELECT * FROM post whre post.id in (123, 456);複製代碼
  • 優點:
    • 讓緩存效率更高。許多應用程序能夠方便地緩存單表查詢時對應的結果對象,這樣能夠減小查詢時的條件。而對MySQL的查詢緩存來講,若是關聯中的某個表發生了變化,那麼久沒法使用查詢緩存了,而拆分後,若是某個表不多改變,那麼基於該表的查詢就能夠重複利用查詢緩存結果。
    • 執行單個查詢能夠減小鎖的競爭。
    • 在應用層作關聯,能夠更容易對數據庫進行拆分,更容易作到高性能和可擴展。
    • 查詢自己效率也可能會有所提高。例如使用IN()代替關聯查詢,可讓MySQL按照ID順序進行查詢,這可能比隨機的關聯要更高效。
    • 能夠減小冗餘記錄的查詢。在應用層作關聯查詢,意味着對於某條記錄應用只須要查詢一次,而在數據庫中作關聯查詢,則可能須要重複地訪問一部分數據。從這點看,這樣的重構還可能會減小網絡和內存的消耗。
    • 更進一步,這樣作至關於在應用中實現了哈希關聯,而不是使用MySQL的嵌套循環關聯。某些場景哈希關聯的效率要高得多。
  • 經過重構查詢將關聯放到應用程序中更加高效的場景:
    • 當應用可以方便地緩存當個查詢的結果
    • 當能夠將數據分佈到不一樣的MySQL服務器上
    • 當可以使用IN()的方式來代替關聯查詢
    • 當查詢中使用同一個數據表的時候

4.4 查詢執行的基礎

當向MySQL發送一個請求的時候,MySQL的工做流程:性能優化

查詢執行路徑
查詢執行路徑

  • 客戶端發送一條查詢給服務器
  • 服務器先檢查查詢緩存,若是命中了緩存,則馬上返回存儲在緩存中的結果。不然進入下一階段
  • 服務器進行SQL解析、預處理,再有優化器生成對應的執行計劃。
  • MySQL根據優化器生成的執行計劃,調用存儲引擎的API來執行查詢
  • 將結果返回給客戶端

4.4.1 MySQL客戶端/服務器通訊協議

  • 客戶端與服務器之間的通訊協議是」半雙工「的,這意味着在任何一個時刻,只能有客戶端或服務器的其中一個發送數據。
    • 限制:
      • 沒辦法進行流量控制,一旦一端開始發送消息,另外一端要接收完整消息才能響應它。
    • 客戶端用一個單獨的數據包將查詢傳給服務器。若是查詢太大,服務器會根據配置max_allowed_packet拒絕更多數據並拋出相應錯誤。
    • 服務器響應給用戶的數據一般更多,由多個數據包組成。當服務器開始響應客戶端請求時,客戶端必須完整地接收整個返回結果。若是隻取前面幾條結果,或者接收幾條結果後就直接斷開鏈接,建議在查詢中加上LIMIT限制。
    • 客戶端多數鏈接MySQL的庫函數(如Java,Python)均可以獲取所有結果集並緩存到內存中,還能夠逐行獲取須要的數據。默認通常是得到所有結果集並緩存到內存中。
      • MySQL一般須要等到全部的數據都已經發送給客戶端才能釋放這條查詢所佔用的資源,因此接收所有結果並緩存經過能夠減小服務器的壓力,讓查詢早點結束而釋放相應資源。
      • 當使用多數鏈接MySQL的庫函數從MySQL獲取數據時,其結果看起來都像是從MySQL服務器獲取數據,而實際上都是從這個庫函數的緩存獲取數據。可是當返回一個很大的結果集時,庫函數會很不少時間和內存來存儲全部的結果集,若是可以儘早開始處理,就能大大減小內存消耗,這種狀況下能夠不使用緩存來記錄結果而是直接處理。這樣作的缺點是,對服務器來講,須要等到查詢完成後才能釋放資源,所以服務器的資源都是被這個查詢佔用。
    • 查詢狀態,對一個MySQL鏈接或者說一個線程,任什麼時候候都有一個狀態。最簡單辦法使用SHOW FULL PROCESSLIST命令查看:
      • SLEEP:線程正在等待客戶端發送新的請求
      • QUERY:線程正在執行查詢或者正在將查詢結果發送給客戶端
      • LOCKED:在MySQL服務器層,該線程正在等待表鎖。在存儲引擎級別實現的鎖,例如InnoDB的行鎖,並不會體如今線程狀態中。對於MyISAM來講這是一個比較典型的狀態,在其餘沒有行鎖的引擎中也常常會出現。
      • Analyzing and statistics:線程正在收集存儲引擎的統計信息,並生成查詢的執行計劃
      • Copying to tmp table [on disk]:線程正在執行操做,而且將其結果集都複製到一個臨時表中,這種狀態通常要麼是在作GROUP BY操做,要麼是文件排序操做,或者是UNION操做。若是這個狀態後面還有」on disk「標記,那標識MySQL正在將一個臨時內存表放到磁盤上。
      • Sorting result:線程正在對結果集進行排序。
      • Sending data:線程可能在多個狀態間傳送數據,或者在生成結果集,或者在向客戶端返回數據。

4.4.2 查詢緩存

  • 在解析一個查詢語句以前,若是查詢緩存是打開的,那麼MySQL會優先檢查這個查詢是否命中查詢緩存的數據。
    • 這個檢查是經過一個大小寫敏感的哈希查找來實現的。
    • 查詢與緩存中的查詢即便只有一個字節不一樣,那也不會匹配緩存結果,這種狀況下查詢就會進入下一階段的處理。
    • 若是當前的查詢剛好命中了查詢緩存,那麼在返回查詢結果以前MySQL會檢查一次用戶權限。這也不須要解析查詢SQL語句的,由於在查詢緩存中已經存放了當前查詢須要訪問的表信息。若是權限沒有問題,MySQL會跳過全部其它的階段,直接從緩存中拿到結果並返回給客戶端。

4.4.3 查詢優化處理

查詢的生命週期的下一步是將一個SQL轉換成一個執行計劃,MySQL再依照這個執行計劃和存儲引擎進行交互,這包括多個子階段:解析SQL、預處理、優化SQL執行計劃。這個過程當中的任何錯誤(例如語法錯誤)均可能終止查詢,另外在實際執行中,這幾部分可能一塊兒執行也可能單獨執行。服務器

  • 語法解析器和預處理:網絡

    • 流程:
      • MySQL經過關鍵字將SQL進行解析,並生成一顆對應的」解析樹「。MySQL解析器將使用MySQL語法規則驗證和解析查詢。例如:驗證是否使用錯誤的關鍵詞,或者使用關鍵詞的順序是否正確等,再或者它還會驗證引號是否能先後正確匹配。
      • 預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這裏將檢查數據表和數據列是否存在,還會解析名字和別名,看看它們是否有歧義。
      • 預處理器會驗證權限。這一般很快,除非服務器上有很是多的權限配置。
  • 查詢優化器:數據結構

    • 通過語法解析器和預處理後,語法樹被認爲是合法的,並將由優化器將其轉化成執行計劃。架構

      • 一條查詢能夠有不少執行方式,最後都返回相同的結果。
      • 優化器的做用就是找到這其中最好的執行計劃。
    • MySQL使用基於成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。

      • 最初,成本的最小單位是隨機讀取一個4K數據頁的成本,後來成本計算公式變得更加複雜,而且引入了一些」因子「來估算這些操做的代價,好比當執行一次WHERE條件比較的成本。
      • 能夠經過SHOW STATUS LIKE 'Last_query_cost';來查詢當前會話的當前查詢的成本,其值N爲MySQL的優化器認爲大概須要作N個數據頁的隨機查找才能完成當前的查詢。
      • Last_query_cost根據一系列統計信息計算得來:每一個表或索引的頁面個數、索引的基數(索引中不一樣值的數量)、索引和數據行的長度、索引的分佈狀況。優化器在評估成本的時候並不考慮任何層面的緩存,它假設讀取任何數據都須要一次磁盤IO。
    • 致使MySQL選擇錯誤的執行計劃的緣由:

      • 統計信息不許確。MySQL依賴存儲引擎提供的統計信息來評估成本,可是有的存儲引擎提供的信息可能誤差很是大。例如,InnoDB由於其MVVC的架構,並不能維護一個數據表的行數的精確統計信息。
      • 執行計劃中的成本估算不等同於實際執行的成本。因此即便統計信息精確,優化器給出的執行計劃也可能不是最優的。例若有時候某個執行計劃雖然須要讀取更多的頁面,可是它的成本卻更小,由於這些頁面可能都是順序讀或者這些頁面已經在內存中,它的訪問成本將很小。
      • MySQL的最優可能和你想的最優不同。你可能但願執行時間儘量短,可是MySQL只是基於其成本模型選擇最優的執行計劃,而有些時候這並非最快的執行方式。因此,這裏咱們看到根據執行成原本選擇執行計劃並非完美的模型。
      • MySQL從不考慮其餘併發執行的查詢,這可能會影響到當前查詢的速度。
      • MySQL也並非任什麼時候候都是基於成本的優化。有時也會基於一些固定的規則,例如,若是存在全文搜索的MATCH()子句,則在存在全文索引的時候就使用全文索引,即便有時候使用別的索引和WHERE條件能夠遠比這種方式要快,MySQL也仍然會使用對應的全文索引。
      • MySQL不會考慮不受其控制的操做成本,例如執行存儲過程或者用戶自定義函數的成本。
      • 優化器有時候沒法去估算全部可能的執行計劃,因此它可能錯過實際上最優的執行計劃。
    • 優化策略:

      • 靜態優化:直接對解析樹進行分析,並完成優化。例如,優化器能夠經過一些簡單的代數變換將WHERE條件轉換成另外一種等價形式。靜態優化不依賴於特別的數值,如WHERE條件中帶入的一些常數等。靜態優化在第一次完成後就一直有效,即便使用不一樣的參數重複執行查詢也不會發生變化。能夠認爲這是一種」編譯時優化「。
      • 動態優化:與上下文有關,也可能和其餘不少因素有關,例如WHER條件中的取值,索引中條目對應的數據行數等。這須要在每次查詢的執行時候都從新評估,甚至在其執行過程當中也會從新優化,能夠認爲這是」運行時優化「。
    • MySQL可以處理的優化類型:

      • 從新定義關聯表的順序:數據表的關聯並不老是按照在查詢中指定的順序執行。決定關聯的順序是優化器很重要的一部分功能。

      • 將外鏈接轉換爲內鏈接:並非全部的OUTER JOIN語句都必須之外鏈接的方式執行。例如WHERE條件,庫表結構均可能會讓外鏈接等價於一個內鏈接。

      • 使用等價變換規則:MySQL使用一些等價變換來簡化並規範表達式。它能夠合併和減小一些比較,還能夠移除一些恆成立和一些恆不成立的判斷。

      • 優化COUNT()、MIN()和MAX():索引和列是否可爲空能夠幫助MySQL優化這類表達式。例如,要找到某一列的最小值,只須要查詢B-Tree索引最左端的記錄,MySQL能夠直接獲取,並在優化器生成執行計劃的時候就能夠利用這一點(優化器會將這個表達式做爲一個常數對待,在EXPLAIN就能夠看到"Select tables optimized away")。相似的,沒有任何WHERE條件的COUNT(*)查詢一般也可使用存儲引擎提供的一些優化(MyISAM維護了一個變量來存放數據表的行數)

      • 預估並轉換爲常數表達式:MySQL檢測到一個表達式能夠轉換爲常數的時候,就會一直把該表達式做爲常數進行優化處理。例如:一個用戶自定義變量在查詢中沒有發生變化、數學表達式、某些特定的查詢(在索引列上執行MIN,甚至是主鍵或惟一鍵查找語句)、經過等式將常數值從一個表傳到另外一個表(經過WHERE、USING或ON來限制某列取值爲常數)。

      • 覆蓋索引掃描:當索引中的列包含全部查詢中全部須要的列的時候,MySQL就可使用索引返回須要的數據,而無須查詢對應的數據行。

      • 子查詢優化:在某些狀況下能夠將子查詢轉換成一種效率更高的形式,從而減小多個查詢屢次對數據的訪問。

      • 提早終止查詢:當發現已經知足查詢的需求,可以馬上終止查詢。例如使用了LIMIT子句,或者發現一個不成立的條件(當即返回一個空結果)。當存儲引擎須要檢索」不一樣取值「或者判斷存在性的時候,例如DISTINCT,NOT EXIST()或者LEFT JOIN類型的查詢,MySQL都會使用這類優化。

      • 等值傳播:若是兩個列的值經過等式關聯,那麼就能夠把其中一個列的WHERE條件傳遞到另外一個列上。

        SELECT film.film_id
        FROM sakila.film
            INNER JOIN sakila.film_actor USING(film_id)
        WHERE film.file_id > 500;
        -- 若是手動經過一些條件來告知優化器這個WHERE條件適用於兩個表,在MySQL中反而讓查詢更難維護。
        ... WHERE film.file_id > 500 AND film_actor.film_id > 500;複製代碼
      • 列表IN()的比較:不一樣於其它數據庫IN()徹底等價於多個OR條件語句,MySQL將IN()列表中的數據先進行排序,而後經過二分查找的方式來肯定列表的值是否知足條件,前者查詢複雜度爲O(n),後者爲O(log n)。對於有大量取值的狀況,MySQL這種處理速度會更快。

  • 數據和索引的統計信息:

    • MySQL架構在服務器層有查詢優化器,但卻沒有保存數據和索引的統計信息。由於統計信息由存儲引擎實現,不一樣的存儲引擎可能會存儲不一樣的統計信息。
    • MySQL查詢優化器在生成查詢的執行計劃時,須要向存儲引擎獲取相應的統計信息。存儲引擎則提供給優化器對應的統計信息,包括:每一個表或索引有多少個頁面、每一個表的每一個索引的基數是多少、數據行和索引的長度、索引的分佈信息等。
  • MySQL如何執行關聯查詢:

    • MySQL認爲任何一個查詢都是管理,而不侷限於須要兩個表的匹配,包括每個查詢,每個片斷(例如子查詢,甚至基於單表的SELECT)
      • UNION查詢的例子:MySQL先將一系列的單個查詢結果放到一個臨時表中,而後再從新讀出臨時表數據來完成UNION查詢。該讀取臨時表結果的操做也是關聯。
    • MySQL關聯執行的策略:對任何關聯都執行嵌套循環關聯操做,即MySQL先在一個表中循環取出單條數據,而後再嵌套循環到下一個表中尋找新配的行,依次下去,直到找到全部表中匹配的行爲止。而後根據各個表匹配的行,返回查詢中須要的各個列。MySQL會嘗試在最後一個關聯表中找到全部匹配的行,若是最後一個關聯表沒法找到更多的行之後,MySQL返回到上一層關聯表,看是否可以找到更多的匹配記錄,依此類推迭代執行。
      • 從本質上來講,全部類型的查詢都以一樣的方式運行。例如,MySQL在FROM子句中遇到子查詢時,先執行子查詢並將其結果放到一個臨時表(MySQL的臨時表是沒有任何索引的,UNION查詢也同樣),而後將這個臨時表看成一個普通表對待。簡而言之,全部的查詢類型都轉換成相似的執行計劃(在MySQL5.6和MariaDB有重大改變,兩個版本都引入更加複雜的執行計劃)
      • 不過不是全部的查詢均可以轉換。例如,全外鏈接就沒法經過嵌套循環和回溯的方式完成,這是當發現關聯表中沒有找到任何匹配行的時候,則多是由於關聯是剛好從一個沒有任何匹配的表開始。這大概也是MySQL並不支持全外鏈接的緣由。
  • 執行計劃:

    • MySQL生成查詢的一顆指令樹,而後經過存儲引擎執行完成這顆指令樹並返回結果,這點和其餘生成查詢字節碼來執行查詢的其它關係數據庫不一樣。
    • 最終的執行計劃包含了重構查詢的所有信息。能夠對查詢執行EXPLAIN EXTENDED後再執行SHOW WARNINGS看到重構的查詢(和原查詢有相同的語義,可是查詢語句可能並不徹底相同)
    • 任何多表查詢均可以用一棵樹來表示,咱們能夠理解爲一顆平衡樹,可是MySQL的執行計劃是一顆左側深度優先的樹。
  • 關聯查詢優化器:

    • MySQL優化器最重要的一部分就是關聯查詢優化,它決定了多個表關聯時的順序。一般多表關聯的時候,能夠有多張不一樣的關聯順序,而關聯查詢優化器則經過評估不一樣順序的成原本選擇一個代價最小的關聯順序。
    • 有的時候,優化器給出的並非最優的關聯順序,這時可使用STRAIGHT_JOIN關鍵字來重寫查詢,讓優化器按照你認爲的最優關聯順序執行——絕大多數時候,優化器作出的選擇都更爲準確。
    • 優化器會盡量遍歷沒一個表而後逐個作嵌套循環計算每一顆可能的執行計劃的樹的成本,最後返回一個最優的執行計劃。
      • 若是有N個表關聯,那麼須要檢查n的階乘種關聯順序。咱們稱之爲全部可能的執行計劃的」搜索空間「。若是搜索空間很是大,當須要關聯的表超過optimizer_search_depth的限制,優化器會選擇使用」貪婪「搜索的方式查找」最優「的關聯方式。
    • 優化器偶爾也會選擇一個不是最優的執行計劃。
    • 有時,各個查詢的順序並不能隨意安排,這時關聯優化器能夠根據這些規則大大減小搜索空間,例如,左鏈接、相關子查詢。這是由於後面的表的查詢須要依賴於前面表的查詢結果,這種依賴關係一般能夠幫助優化器大大減小須要掃描的執行計劃數量。
  • 排序優化

    • 不管如何排序都是一個成本很高的操做,因此從性能角度考慮,應儘量避免排序或者儘量避免對大量數據進行排序。

    • 文件排序:當不能使用索引生成排序結果的時候,MySQL須要本身進行排序,若是數據量小則在內存中進行,若是數據量大則須要使用磁盤。

      • 若是須要排序的數據量小於」排序緩衝區「,MySQL使用內存進行」快速排序「操做。
      • 若是內存不夠排序,MySQL會先將數據分塊,對每一個獨立的塊使用」快速排序「進行排序,並將各個塊的排序結果存放在磁盤上,而後將各個排序好的塊進行合併,最後返回排序結果。
    • 排序算法:

      • 兩次傳輸排序(舊版本使用):讀取行指針和須要排序的字段,對其進行排序,而後在根據排序結果讀取須要的數據行。這須要兩次數據傳輸,即須要從數據表中讀取兩次數據,第二次讀取數據的時候,由於讀取順序列進行排序後的全部記錄,這會產生大量的隨機IO,因此成本很是高。當使用的是MyISAM表的時候,成本可能會更高,由於MyISAM使用系統調用進行數據讀取(很是依賴操做系統對數據的緩存)。不過這樣作的優勢是:排序的時候存儲儘量少的數據,讓」排序緩衝區「中儘量容納更多的行數進行排序。
      • 單次傳輸排序(4.1後新版本使用):先讀取查詢所須要的全部列,而後再根據給定列進行排序,最後直接返回排序結果。有點事只須要一次順序IO讀取全部的數據,而無須任何的隨機IO,缺點是若是須要返回的列很是大,會額外佔用大量的空間,而這些列對排序操做來講是沒有任何做用的。由於單條排序記錄很大,因此可能會有更多的排序塊須要合併。
      • 很難說以上兩個算法效率更高,當查詢須要全部的列的總長度不超過max_lenght_for_sort_data時,MySQL使用「單次傳輸排序」,能夠經過參數選擇來影響MySQL排序算法的選擇。
    • 進行文件排序的時候須要使用的臨時存儲空間可能會比想象的要大得多。緣由在於MySQL排序時,對每個排序記錄都會分配一個足夠長的定長空間來存放。

      • 這個定長空間必須足夠長以容納其中最長的字符串。若是是VARCHAR列則須要分配其完整長度,若是使用UTF-8字符集,則爲每一個字符預留三個字節。
    • 在關聯查詢的時候排序:

      • 若是ORDER BY子句中全部列都來自關聯的第一個表,那麼MySQL在關聯處理第一個表的時候進行文件排序。能夠在EXPLAIN看到Extra字段有「Using filesort」

      • 除第一種場景,MySQL都會先將關聯的結果放到一個臨時表中,而後在全部的關聯都結束後,再進行文件排序操做。用EXPLAIN可看到「Using temporary;Using filesort」。若是查詢中有LIMIT的話,LIMIT也會在排序以後應用,因此即便須要返回較少的數據,臨時表和須要排序的數據仍然很是大。

        5.6後版本在這裏作了些改進:當只須要返回部分排序結果的時候,例如使用了LIMIT子句,MySQL再也不對全部的結果進行排序,而是根據實際狀況,選擇拋棄不知足條件的結果,而後在進行排序。

4.4.4 查詢執行引擎

  • 在解析和優化階段,MySQL將生成查詢對應的執行計劃,MySQL的查詢執行引擎則根據這個執行計劃來完成整個查詢。

    • 這裏的執行計劃是一個數據結構,不一樣於其餘關係數據庫生成的字節碼。
  • 查詢執行階段不是那麼複雜,MySQL只是簡單地根據執行計劃給出的指令逐步執行。在根據執行計劃逐步執行的過程當中,又大量的操做須要調用存儲引擎實現的「handle API」接口來完成。

    • 查詢中的每個表由一個handler實例來標識。實際上,MySQL在優化階段就爲每一個表建立了一個handler實例,優化器根據這些實例的接口能夠獲取表的相關信息。
    • 存儲引擎的接口有着很是豐富的功能,可是底層接口卻只有十幾個,這些接口相互配合可以完成查詢的大部分操做
  • 並非全部的操做都有handler完成。例如,當MySQL須要進行表鎖的時候,handler可能會實現本身級別的、更細粒度的鎖,如InnoDB就實現了本身的行基本鎖,但這並不能代替服務器層的表鎖。若是是全部存儲共有的特性則由服務器層實現,如時間、日期函數、視圖和觸發器等。

4.4.5 返回結果給客戶端

  • 查詢執行的最後一個階段。即便查詢不須要返回結果集給客戶端,MySQL仍然會返回這個查詢的一些信息,如該查詢影響到的行數。
  • 若是查詢能夠被緩存,那麼MySQL在該階段也會將結果放到查詢緩存中。
  • 結果集返回客戶端是一個增量、逐步返回的過程。例如,服務器處理完關聯操做的最後一個關聯表,開始生成第一條結果時,MySQL就能夠開始向客戶端逐步返回結果。
    • 好處:
      • 服務器端無須存儲太多的結果,也就不會由於要返回太多的結果而消耗太多的內存。
      • 讓客戶端第一時間得到返回的結果。可以使用SQL_BUFFER_RESULT設置。
    • 結果集中的每一行都會以一個知足MySQL客戶端/服務器通訊協議的封包發送,再經過TCP協議進行傳輸,在TCP傳輸的過程當中,可能對MySQL的封包進行緩存而後批量傳輸。

4.5 MySQL查詢優化器的侷限性

MySQL的萬能「嵌套循環」並非對每種查詢都是最優的,但只對少部分查詢不適用,咱們每每能夠經過改寫查詢讓MySQL高效地完成工做。另外,5.6版本會消除不少本來的限制,讓更多的查詢可以已儘量高的效率完成。

4.5.1 關聯子查詢

MySQL的子查詢實現得很是糟糕,最糟糕的一類查詢是WHERE條件語句中包含IN()的子查詢。

SELECT * FROM sakila.film
WHERE film_id IN(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1 );
-- MySQL對IN()列表中的選項有專門的優化策略,但關聯子查詢並非這樣的,MySQL會將相關的外層表壓到子查詢中,它認爲這樣能夠高效地查找到數據行。也就是說,以上查詢會被MySQL更改爲:
SELECT * FROM sakila.film
WHERE EXISTS(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1
    AND film_actor.film_id = film.film_id);
-- 這時子查詢須要根據film_id來關聯外部表的film,由於須要film_id字段,因此MySQL認爲沒法先執行這個子查詢。經過EXPLIAN能夠看到子查詢是一個相關子查詢(DEPENDENT SUBQUERY),而且能夠看到對film表進行全表掃描,而後根據返回的film_id逐個進行子查詢。若是外層是一個很大的表,查詢性能會很糟糕。
-- 優化重寫方式1:
SELECT film.* FROM sakila.film
    INNER JOIN sakil.film_actor USING(film_id) 
WHERE actor_id =1;
-- 優化重寫方式2:使用函數GROUP_CONCAT()在IN()中構造一個逗號分割的列表。
-- 優化重寫方式3,使用EXISTS()等效的改寫查詢:
SELECT * FROM sakila.film
WHERE EXISTS(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1
    AND film_actor.film_id = film.film_id);複製代碼
  • 如何用好關聯子查詢
    • 並非全部的關聯子查詢性能都不好,須要先測試再作出判斷。不少時候,關聯子查詢是一種很是合理、天然、甚至是性能最好的寫法。

4.5.2 UNION的限制

  • 有時,MySQL沒法將限制條件從外層「下推」到內層,這使得原表可以限制部分返回結果的條件沒法應用到內層查詢的優化上。

  • 若是但願UNION的各個子句可以根據LIMIT只取部分結果集,或者但願可以先排好序再合併結果集的話,就須要在UNION的各個子句中分別使用這些子句。另外,從臨時表取出數據的順序是不必定的,若是要得到正確的順序,還須要加上一個全局的ORDER BY 和 LIMIT

    (SELECT first_name, last_name
     FROM sakila.actor
     ORDER BY last_name)
    UNION ALL
    (SELECT first_name, last_name
     FROM sakila.customer
     ORDER BY last_name)
    LIMIT 20;
    -- 在UNION子句分別使用LIMIT
    (SELECT first_name, last_name
     FROM sakila.actor
     ORDER BY last_name
     LIMIT 20)
    UNION ALL
    (SELECT first_name, last_name
     FROM sakila.customer
     ORDER BY last_name
     LIMIT 20)
    LIMIT 20;複製代碼

4.5.3 索引合併優化

  • 5.0及其新版本,當WHERE條件包含多個複雜條件的時候,MySQL可以訪問當個表的多個索引以合併和交叉過濾來定位須要查找的行。

4.5.4 等值傳遞

  • 某些時候,等值傳遞會帶來一些意想不到的額外消耗。例如,有一個很是大的IN()列表,而MySQL優化器發現存在WHERE、ON或者USING的子句,將這個列表的值和另外一個表的某個列相關聯。
    • 優化器會將IN()列表都複製應用到關聯的各個表中。一般各個表由於新增了過濾條件,優化器能夠更高效地從存儲引擎過濾記錄。可是若是列表很是大,則會致使優化和執行都會變得更慢。

4.5.5 並行執行

  • MySQL沒法利用多核特性來並行執行查詢,這點和其餘關係型數據庫不同。

4.5.6 哈希關聯

  • MySQL並不支持哈希關聯——全部的關聯都是嵌套循環關聯。不過,能夠經過創建一個哈希索引來曲線實現哈希關聯。
  • 若是使用的是Memory存儲引擎,則索引是哈希索引,因此關聯的時候也相似於哈希關聯。

4.5.7 鬆散索引掃描

  • MySQL並不支持鬆散索引掃描,也就沒法按照不連續的方式掃描一個索引。一般,MySQL的索引掃描須要先定義一個起點和終點,即便須要的數據只是這段索引中不多數的幾個,MySQL仍須要掃描這段索引中每個字段。

  • 示例:假設咱們有索引(a,b),有如下查詢SELECT ... FROM tb1 WHERE b BETEWEEN 2 AND 3;,由於只使用了字段b而不符合索引的最左前綴,MySQL沒法使用這個索引,從而只能經過全表掃描找到匹配的行。

    MySQL經過全表掃描找到須要的記錄
    MySQL經過全表掃描找到須要的記錄

    瞭解索引結構的話,會發現還有一個更快的辦法執行上面的查詢。索引的物理結構(不是存儲引擎API)使得能夠先掃描a列第一個值對應的b列的範圍,而後在跳到a列第二個只掃描對應的b列的範圍,即鬆散索引掃描。這時就無須再使用WHERE過濾,由於已經跳過了全部不須要的記錄。MySQL並不支持鬆散索引掃描

    鬆散索引掃描
    鬆散索引掃描

  • MySQL5.0 之後的版本,某些特殊的場景下是可使用鬆散索引掃描的。例如,在一個分組查詢中須要找到分組的最大值和最小值:

    -- 在Extra字段顯示「Using index for group-by」,表示使用鬆散索引掃描
    EXPLAIN SELECT actor_id, MAX(film_id)
    FROM sakila.film_actor
    GROUP BY actor\G;複製代碼
  • 在MySQL很好地支持鬆散索引掃描以前,一個簡單的繞過辦法就是給前面的列加上可能的常數值。5.6以後的版本,關於鬆散索引掃描的一些限制將會經過「索引條件下推(index condition pushdown)」的方式解決

4.5.8 最大值和最小值優化

  • 對於MIN()和MAX()查詢,MySQL的優化作得並很差。

    SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';
    -- 由於在first_name上沒有索引,MySQL將會進行一次全表掃描。若是MySQL可以進行主鍵掃描,那麼理論上當MySQL讀到第一個知足條件的記錄,就是須要找到的最小值,由於主鍵是嚴格按照actor_id字段的大小順序排列的。
    -- 曲線優化辦法:移除MIN(),而後使用LIMIT
    SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENNLOPE' LIMIT 1;
    -- 該SQL已經沒法表達它的本意,通常咱們經過SQL告訴服務器須要什麼數據,再由服務器決定如何最優地獲取數據。但有時候爲了得到更高的性能,須要放棄一些原則。複製代碼

4.5.9 在同一個表查詢和更新

  • MySQL不容許對同一張表同時進行查詢和更新。這其實並非優化器的限制,若是清楚MySQL是如何執行查詢的,就能夠避免這種狀況。能夠經過生成表來繞過該限制。

    -- 符合標準的SQL,可是沒法運行
    mysql> UPDATE tbl AS outer_tbl
    -> SET cnt = (
    -> SELECT count(*) FROM tbl AS inner_tbl
    -> WHERE inner_tbl.type = outer_tbl.type
    -> );
    -- 生成表來繞過該限制:
    mysql> UPDATE tbl
    -> INNER JOIN(
    -> SELECT type, count(*) AS cnt
    -> FROM tbl
    -> GROUP BY type
    -> ) AS der USING(type)
    -> SET tbl.cnt = der.cnt;複製代碼

4.6 查詢優化器的提示(hint)

若是對優化器選擇的執行計劃不滿意,可使用優化器提供的幾個提示(hint)來控制最終的執行計劃。不過MySQL升級後可能會致使這些提示無效,須要從新審查。

  • 部分提示類型:

    • HIGH_PRIORITY和LOW_PRIORITY:

      告訴MySQL當多個語句同時訪問某一個表的時候,這些語句的優先級。只對使用表鎖的存儲引擎有效,但即便是在MyISAM中也要慎重,由於這兩個提示會致使併發插入被禁用,可能會致使嚴重下降性能

      • HIGH_PRIORITY:用於SELECT語句時,MySQL會將此語句從新調度到全部正在等待表鎖以便修改數據的語句以前。其實是將其放在表的隊列的最前面,而不是按照常規順序等待。用於INSERT語句,其效果只是簡單地抵消了全局LOW_PRIORITY設置對該語句的影響。
      • 用於SELECT、INSERT、UPDATE和DELETE語句,讓該語句一直處於等待狀態,只要隊列中還有須要訪問同一個表的語句——即便那些比該語句還晚提交到服務器的語句。
    • DELAYED:

      • 只對INSERT和REPLACE有效。
      • MySQL會將使用該提示的語句當即返回給客戶端,並將插入的行數據放入到緩衝區,而後在表空閒時批量將數據寫入。
      • 日誌系統使用這樣的提示很是有效,或者是其餘須要寫入大量數據可是客戶端卻不須要等待單條語句完成IO的應用。
      • 限制:並非全部的存儲引擎都支持;而且該提示會致使函數LAST_INSERT_ID()沒法正常工做
    • STRAIGHT_JOIN:

      當MySQL沒能正確選擇關聯順序的時候,或者因爲可能的順序太多致使MySQL沒法評估全部的關聯順序的時候,STRAIGNT_JOIN都會頗有用。特別是在如下第二種狀況,MySQL可能會花費大量時間在」statistics「狀態,加上這個提示會大大減小優化器的搜索空間。

      能夠先使用EXLPAN語句來查看優化器選擇的關聯順序,而後使用該提示來重寫查詢,肯定最優的關聯順序。可是在升級MySQL的時候,要從新審視這類查詢。

      • 放置在SELECT語句的SELECT關鍵字以後:讓查詢中全部的表按照在語句中出現的順序進行關聯
      • 放置在任何兩個關聯表的名字之間:固定其先後兩個表的關聯順序。
    • SQL_SMALL_RESULT和SQL_BIG_RESULT:

      • 只對SELECT語句有效,告訴優化器對GROUP BY或者DISTINCT查詢如何使用臨時表和排序。
      • SQL_SMALL_RESULT告訴優化器結果集很小,能夠將結果集放在內存中的索引臨時表,避免排序操做
      • SQL_BIG_RESULT告訴優化器結果集可能會很是大,建議使用磁盤臨時表作排序操做
    • SQL_BUFFER_RESULT:

      • 告訴優化器將查詢結果放入到一個臨時表,而後儘量地釋放表鎖。
    • SQL_CACHE和SQL_NO_CACHE

      • 告訴MySQL這個結果集是否應該緩存在查詢緩存中
    • SQL_CALC_FOUND_ROWS:

      • 嚴格來講,並非一個優化器提示,它不會告訴優化器任何關於執行計劃的東西。
      • 讓MySQL返回的結果集包含更多信息。
      • 查詢中加上該提示MySQL會計算除去LIMIT子句後這個查詢要返回的結果集的總數,而實際上只返回LIMIT要求的結果集
    • FOR UPDATE和LOCK IN SHARE MODE

      • 也不是真正的優化器提示。
      • 主要控制SELECT語句的鎖機制,但只對實現了行級鎖的存儲引擎有效。
      • 會對符合查詢條件的數據行加鎖。對於INSERT...SELECT語句是不須要這兩個提示的,由於5.0及新版本會默認加鎖。
      • InnoDB是惟一內置支持這兩個提示的引擎。可是這兩個提示會讓某些優化沒法正常使用,例如索引覆蓋掃描。InnoDB不能在不訪問主鍵的狀況下排他地鎖定行,由於行的版本信息保存在主鍵中。
      • 這兩個提示常常被濫用,很容易形成服務器的鎖爭用問題,應該儘量避免使用。
    • USING INDEX、IGONRE INDEX和FORCE INDEX:

      • 告訴優化器使用或不使用哪些索引來查詢記錄(例如,在決定關聯順序的時候使用哪一個索引)。
      • 5.1及新版本能夠經過FOR ORDER BY和FOR GROUP BY來制定是否對排序和分組有效。
      • USING INDEX和FORCE INDEX基本相同。可是FORCE INDEX會告訴優化器全表掃描的成本會遠遠高於索引掃描,哪怕實際上該索引的用處不大。當發現優化器選擇了錯誤的索引,或者由於某些緣由(好比在不適用ORDER BY的時候但願結果有序)要使用另外一個索引時,可使用該提示。
  • 5.0和更新版本新增用來控制優化器行爲的參數:

    • optimizer_search_depth:控制優化器在窮舉執行計劃時的限度。若是查詢長時間處於"Statistics"狀態,能夠考慮調低
    • optimizer_prune_level:默認打開,讓優化器根據須要掃描的行數來決定是否跳過某些執行計劃
    • optimizer_swith:包含了一些開啓/關閉優化器特性的標誌位。

4.7 優化特定類型的查詢

4.7.1 優化count()查詢

  • count()的做用:

    • 統計某個列值的數量,要求列值是非空的(不統計NULL)。若是在COUNT()的括號中指定了列或者列的表達式,則統計的就是這個表達式有值的結果數(而不是NULL)
    • 統計行數。當MySQL確認括號內的表達式值不可能爲空時,實際上就是統計行數。
      • 當使用COUNT()時,並不會像咱們猜測的擴展成全部的列,實際上,它會忽略全部的列而直接統計全部的行數。
      • 常見錯誤:在括號內指定了一個列卻但願統計結果集的行數。
  • 關於MyISAM的神話:

    • 只有沒有任何WHERE條件的count(*),MyISAM的COUNT()函數纔會很是快,由於此時無須實際地去計算表的行數。MySQL能夠利用存儲引擎的特性直接得到這個值。
    • 若是MySQL知道某列col不可能爲NULL值,那麼內部會把count(col)優化成count(*)
    • 當統計帶有WHERE子句,MyISAM的COUNT()和其餘存儲引擎沒有任何不一樣,就不會再有神話般的速度。
  • 簡單的優化

    • 利用MyISAM在count(*)全表很是快的特性,來加速一些特定條件的查詢。

      -- 使用標準數據據worold
      SELECT count(*) FROM world.city WHERE ID > 5;
      -- 將條件反轉,可很大程度減小掃描行數到5行之內
      SELECT (SELECT count(*) FROM world.city) - COUNT(*) 
      FROM world.city WHERE ID <= 5;複製代碼
    • 示例:假設可能須要經過一個查詢返回各類不一樣顏色的商品數量

      -- 使用SUM
      SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0)) AS red FROM items;
      -- 使用COUNT,只須要將知足條件的設置爲真,不知足設置爲NULL
      SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULLASred FROM items;複製代碼
  • 使用近似值:

    • 有時候某些業務場景並不要求徹底精確的COUNT值,此時能夠用近似值來代替。
    • EXPLAIN出來的優化器估算的行數就是一個不錯的近似值,執行EXPLAIN並不須要真正地去執行查詢,因此成本很低。
    • 例如:若是要精確統計網站在線人數,一般WHERE條件會很複雜,一方面須要過濾當前非活躍用戶,另外一方面還須要過濾系統中某些特定ID的默認用戶,去掉這些約束條件對總數的影響很小,但卻可能很好地提升該查詢的性能。更進一步優化則能夠嘗試刪除DISTINCT這樣的約束來避免文件排序。這樣重寫的查詢比精確查詢要快得多,而返回的結果則幾乎相同。
  • 更復雜的優化:

    • 一般來講,COUNT()都須要掃描大量的行(意味着要訪問大量數據)才能得到精確的結果,所以是很難優化的。
    • 優化方法:
      • 前面提到的方法
      • 在MySQL層面能作的只有索引覆蓋掃描
      • 考慮修改應用架構,能夠增長彙總表,或者相似Memcached這樣的外部緩存系統。可能很快你就會發現陷入到一個熟悉的困境,」快速、精確和實現簡單「,三者永遠只能知足其二,必須舍掉其中一個。

    4.7.2 優化關聯查詢

    • 確保ON或者USING子句的列上有索引。
      • 在建立索引的時候就須要考慮關聯的順序。當表A和表B用列c關聯的時候,入股優化器的關聯順序是B、A,那麼就不須要在B表的對應列上建立索引。沒有的索引智慧帶來額外的負擔。
      • 通常來講,除非有其餘理由,不然只須要在關聯順序的第二個表的響應列上建立索引。
    • 確保任何的GROUP BY和ORDER BY中的表達式只涉及到一個表中的列,這樣MySQL纔有可能使用索引來優化這個過程。
    • 當升級MySQL的時候須要注意:關聯語法、運算符優先級等其餘可能會發生變化的地方。由於之前是普通關聯的地方可能會變成笛卡兒積,不一樣類型的關聯可能會生成不一樣的結果。

4.7.3 優化子查詢

  • 儘量使用關聯查詢代替。這並非絕對的,5.6及新版本或者MariaDB,能夠直接使用子查詢。

4.7.4 優化GROUP BY和DISTINCT

  • 在不少場景下,MySQL都使用一樣的辦法優化這兩種查詢,事實上,MySQL優化器會在內部處理的時候相互轉換這兩類查詢。它們均可以使用索引來優化,這也是最有效的優化辦法。
  • 當沒法使用索引時,GROUP BY使用兩種策略:使用臨時表或者文件排序來作分組。能夠經過提示SQL_BIG_RESULT和SQL_SMALL_RESULT來讓優化器按照你但願的方式運行。
  • 若是須要對關聯查詢作分組GROUP BY,而且是按照查找表中的某個列進行分組,那麼一般採用查找表的標識列分組的效率會比其餘列更高
    • SELECT後面出現的非分組列必定是直接依賴於分組列,而且在每一個組內的值是惟一的,或者是業務上根本不在意這個值具體是什麼。
    • 在分組查詢的SELECT中直接使用非分組列一般不是什麼好主意,由於這樣的結果一般是不定的,當索引改變,或者優化器選擇不一樣的優化策略時均可能致使結果不同。
  • 若是沒有經過ORDER BY子句顯示地制定排序列,當查詢使用GROUP BY子句的時候,結果集會自動按照分組的字段進行排序。若是不關心結果集的排序,而這種默認排序又致使了須要文件排序,則可使用ORDER BY NULL,讓MySQL再也不進行文件排序。
  • 優化GROUP BY WITH ROLLUP:
    • 分組查詢的一個變種就按要求MySQL對返回的分組結果在作一次超級聚合。可使用WITH ROLLUP子句來實現這種邏輯,但可能會不夠優化。
      • 不少時候,若是能夠在應用程序中作超級聚合是更好的,雖然這須要返回給客戶端更多的結果。
      • 也能夠在FROM子句中嵌套使用子查詢,或者是經過一個臨時表存放中間數據,而後和臨時表執行UNION來獲得最終結果
      • 最好的辦法是儘量將WITH ROLLUP功能轉移到應用程序中處理。

4.7.5 優化LIMIT分頁

  • 在系統中須要進行分頁操做的時候,一般會使用LIMIT加上偏移量的辦法來實現,同時加上合適的ORDER BY子句。若是有對應的索引,一般效率會很不錯,不然,MySQL須要作大量的文件排序操做。
  • 偏移量很是大的時候優化辦法:
    • 在頁面中限制分頁的數量
    • 優化大偏移量的性能:
      • 儘量使用覆蓋索引掃描,而不是查詢全部的列。例如使用延遲關聯
      • 有時能夠將LIMIT查詢轉換爲已知位置的查詢,讓MySQL經過範圍掃描得到對應的結果。
      • 使用」書籤」記錄上一次取數據的位置。
      • 使用預先計算的彙總表,或者關聯到一個冗餘表,冗餘表只包含主鍵列和須要作排序的數據列。

4.7.6 優化SQL_CALC_FOUND_ROWS

分頁的時候,另外一個經常使用的技巧是在LIMIT語句中加上SQL_CALC_FOUND_ROWS提示,這樣就能夠得到去掉LIMIT之後知足條件的行數,所以能夠做爲分頁的總數。加上這個提示後,MySQL無論是否須要都會掃描全部知足條件的行,而後拋棄掉不須要的行,而不是在知足LIMIT的行數後就終止掃描。因此該提示的代價可能很是高。

  • 設計方案1:將具體的頁數換成「下一頁」按鈕,假設每頁顯示20條記錄,那麼使用LIMIT返回21條並只顯示20條,若是第21條存在,那麼顯示「下一頁」按鈕,不然說明沒有更多的數據,也就無須顯示
  • 設計方案2:先獲取並緩存較多的數據,而後每次分頁都從這個緩存中獲取。
  • 其餘設計方案:有時候也能夠考慮使用EXPLAIN的結果中的rows列的值來做爲結果集總數的近似值(實際上Google的搜索結果總數也是個近似值)。當須要精確結果時,再單獨使用COUNT(*)來知足需求,這時候若是可以使用覆蓋索引則一般也會比SQL_CALC_FOUND_ROWS快得多。

4.7.7 優化UNION查詢

  • MySQL老是經過建立並填充臨時表的方式來執行UNION查詢。所以不少優化策略都無法很好的使用。常常須要手動地將WHERE、LIMIT、ORDER BY子句下推UNION的各個子查詢中,以便優化器能夠充分利用這些條件進行優化
  • 除非確實須要服務器消除重複的行,不然就必定要使用UNION ALL。
    • 若是沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會致使對整個臨時表作惟一性檢查,這樣作的代價很是高。
    • 即便有ALL關鍵字,MySQL仍然會使用臨時表存儲結果。

4.7.8 靜態查詢分析

Percona Toolkit contains pt-query-advisor, a tool that parses a log of queries, analyzes
the query patterns, and gives annoyingly detailed advice about potentially bad practices
in them.

4.7.9 使用用戶自定義變量

  • 用戶自定義變量是一個用來存儲內容的臨時容器,在鏈接MySQL的整個過程當中都存在。在查詢中混合使用過程化和關係化邏輯的時候,該特性很是有用。

  • 使用方法:

    SET @one       := 1;
    SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
    SET @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
    SELECT ... WHERE col <= @last_week;
    -- 具備「左值」特性,在給一個變量賦值的同時使用這個變量
    SELECT actor_id, @rownum := @rownum + 1 As rownum ...複製代碼
  • 沒法使用的場景:

    • 使用自定義變量的查詢,沒法使用查詢緩存
    • 不能在使用常量或者標識符的地方使用自定義變量,例如表名、列名和LIMIT子句中。
    • 用戶自定義變量的生命週期是在一個鏈接中有效,因此不能用它們來作鏈接中的通訊。
    • 若是使用鏈接池或者持久化鏈接,自定義變量可能讓看起來毫無關係的代碼交互(若是是這樣,一般是代碼或鏈接池bug)
    • 在5.0以前的版本,是大小寫敏感的,因此要注意代碼在不一樣MySQL版本間的兼容性問題。
    • 不能顯示地聲明自定義變量的類型。肯定未定義變量的具體類型的時機在不一樣MySQL版本中也可能不同。若是但願是整形/浮點/字符串,最好初始化時0/0.0/‘’。MySQL的用戶自定義變量是一個動態類型,在賦值的時候會改變。
    • MySQL優化器在某些場景下可能會將這些變量優化掉,這可能致使代碼不按預想的方式運行。
    • 賦值的順序和賦值的時間點並不老是固定的,這依賴於優化器的決定。實際狀況可能讓人困惑。
    • 賦值符號:=的優先級很是低,因此要注意,賦值表達式應該使用明確的括號。
    • 使用未定義變量不會產生任何語法錯誤,若是沒注意到這一點,很是容易犯錯。
  • 應用場景:

    • 優化排名語句:

      -- 查詢獲取演過最多電影的前10位演員,而後根據出演電影次數作一個排名,若是出演次數同樣,則排名相同。
      mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0;
      -> SELECT actor_id,
      -> @curr_cnt := cnt AS cnt,
      -> @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
      -> @prev_cnt := @curr_cnt AS dummy
      -> FROM (
      -> SELECT actor_id, COUNT(*) AS cnt
      -> FROM sakila.film_actor
      -> GROUP BY actor_id
      -> ORDER BY cnt DESC
      -> LIMIT 10
      -> ) as der;複製代碼
    • 避免重複查詢剛剛更新的數據:

      -- 在更新行的同時又但願獲取獲得該行的信息。雖然看起來仍然須要兩個查詢和兩次網絡來回,但第二個查詢無須訪問任何數據表,速度會快不少
      UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
      SELECT @now;複製代碼
    • 統計更新和插入的數量

      -- 使用了INSERT ON DUPLICATE KEY UPDATE的時候,想統計插入了多少行的數據,而且有多少數據是由於衝突而改寫成更新操做。
      -- 實現該辦法的本質以下,當每次因爲衝突致使更新時對變量@x自增一次,而後經過對這個表達式乘以0來讓其不影響要更新的內容
      INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
      ON DUPLICATE KEY UPDATE
      c1 = VALUES(c1) + ( 0 * ( @x := @x +1 ) );複製代碼
    • 肯定取值的順序

      • 一個最多見的問題,沒有注意到在賦值和讀取變量的使用多是在查詢的不一樣階段。

        -- WHERE和SELECT是在查詢執行的不一樣階段被執行的,而WHERE是在ORDER BY文件排序操做以前執行。
        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
        -> FROM sakila.actor
        -> WHERE @rownum <= 1;
        +----------+------+
        | actor_id | cnt  |
        +----------+------+
        | 1        | 1    |
        | 2        | 2    |
        +----------+------+複製代碼
      • 儘可能讓變量的賦值和取值發生在執行查詢的同一個階段。

        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, @rownum AS rownum
        -> FROM sakila.actor
        -> WHERE (@rownum := @rownum + 1) <= 1;複製代碼
      • 將賦值運距放到LEAST(),這樣就能夠徹底不改變排序順序的時候完成賦值操做。這個技巧在不但願對子句的執行結果有影響卻又要完成變量複製的時候頗有用。這樣的函數還有GREATEST(), LENGTH(), ISNULL(), NULLIF(), IF(), 和COALESCE()。

        -- LEAST()老是返回0
        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, first_name, @rownum AS rownum
        -> FROM sakila.actor
        -> WHERE @rownum <= 1
        -> ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);複製代碼
  • 編寫偷懶的UNION:

    假設須要編寫一個UNION查詢,其第一個子查詢做爲分支條件先執行,若是找到了匹配的行,則跳過第二個分支。在某些業務場景中確實會有這樣的需求,好比如今一個頻繁訪問的表中查找「熱」數據,找不到再去另一個較少訪問的表中查找「冷數據「。(區分熱冷數據是一個很好提升緩存命中率的辦法)。

    -- 在兩個地方查找一個用戶,一個主用戶表,一個長時間不活躍的用戶表,不活躍的用戶表的目的是爲了實現更高效的歸檔。
    -- 舊的UNION查詢,即便在users表中已經找到了記錄,上面的查詢仍是會去歸檔表中再查找一次。
    SELECT id FROM users WHERE id = 123
    UNION ALL
    SELECT id FROM users_archived WHERE id = 123;
    -- 用一個偷懶的UINON查詢來抑制這樣的數據返回,當第一個表中沒有數據時,咱們纔在第二個表中查詢。一旦在第一個表中找到記錄,就定義一個變量@found,經過在結果列中作一次賦值來實現,而後將賦值放在函數GREATEST中來避免返回額外的數據。爲了明確結果來自哪個表,新增了一個包含表名的列。最後須要在查詢的末尾將變量重置爲NULL,保證遍歷時不干擾後面的結果。
    SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl
    FROM users WHERE id = 1
    UNION ALL
    SELECT id, 'users_archived'
    FROM users_archived WHERE id = 1 AND @found IS NULL
    UNION ALL
    SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;複製代碼
  • 用戶自定義變量的其餘用處:

    • 不只是在SELECT語句中,在其餘任何類型的SQL語句中均可以對變量進行賦值。例如,能夠像前面使用子查詢的方式改進排名語句同樣來改進UPDATE語句。
    • 有時須要使用一些技巧來得到但願的結果。由於優化器可能會把變量看成一個編譯時常量來對待,而不是對其進行賦值。將函數放在相似LEAST()這樣的函數中一般能夠避免這樣的問題。另外一個辦法是在查詢被執行前檢查變量是否被賦值。
  • 其餘用法:

    • 查詢運行時計算總數和平均值
    • 模擬GROUP語句中的函數FIRST()和LAST()
    • 對大量數據作一些數據計算。
    • 計算一個大表的MD5散列值
    • 編寫一個樣本處理函數,當樣本中的數值超過某個邊界值的時候將其變爲0
    • 模擬讀/寫遊標
    • 在SHOW語句的WEHRE子句中加入變量值。

4.8 案例學習

4.8.1 使用MySQL構建一個隊列表

使用MySQL來實現對列表是一個取巧的作法,不少系統在高流量、高併發的狀況下表現並很差。典型的模式是一個表包含多種類型的記錄:未處理記錄、已處理記錄、正在處理的記錄等等。一個或者多個消費者線程在表中查找未處理的記錄,而後聲稱正在處理,當處理完成後,再將記錄更新爲已處理狀態。通常的,例如郵件發送、多命令處理、評論修改等會使用相似模式,但

原有處理方式不合適的緣由:

  • 隨着對列表愈來愈大和索引深度的增長,找到未處理記錄的速度會隨之變慢。
  • 通常的處理過程分兩步,先找到未處理的記錄而後加鎖。找到記錄會增長服務器的壓力,而加鎖操做則會讓各個消費者進程增長競爭,由於這是一個串行化操做。

優化過程:

  • 將對列表分紅兩部分,即將已處理記錄歸檔或者存放到歷史表,這樣始終保證對列表很小。

  • 找到未處理記錄通常來講都沒問題,若是有問題則能夠經過使用消息方式來通知各個消費者。

    • 可已使用一個帶有註釋的SLEEP()函數作超時處理。這讓線程一直阻塞,直到超時或者另外一個線程使用KILL QUERY結束當前的SLEEP。所以,當再向對列表中新增一批數據後,能夠經過SHOW PROCESSLIST,根據註釋找到當前正在休眠操做的線程,並將其KILL。可使用函數GET_LOCK和RELEASE_LOCK()來實現通知,或者能夠在數據庫以外實現,如使用一個消息服務。

      SELECT /* waiting on unsent_emails */ SLEEP(10000), col1 FROM table;複製代碼
  • 最後一個問題是如何讓消費者標記正在處理的記錄,而不至於讓多個消費者重複處理一個記錄。

    • 儘可能避免使用SELECT FOR UPDATE,這一般是擴展性問題的根源,這會致使大量的書屋阻塞並等待。不光是隊列表,任何狀況下都要避免。

    • 能夠直接使用UPDATE來更新記錄,而後檢查是否還有其餘的記錄須要處理。(全部的SELECT FOR UPDATE均可以使用相似的方式改寫)

      -- 該表的owner用來存儲當前正在處理這個記錄的鏈接ID,即由函數CONNECTION_ID()返回額ID,若是當前記錄沒有被任何消費者處理,則該值爲0
      CREATE TABLE unsent_emails (
          id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
          -- columns for the message, from, to, subject, etc.
          status ENUM('unsent', 'claimed', 'sent'),
          owner INT UNSIGNED NOT NULL DEFAULT 0,
          ts TIMESTAMP,
          KEY (owner, status, ts)
      );
      -- 常見的處理辦法。這裏的SELECT查詢使用到索引的兩個列,理論上查找的效率應該更快。問題是,兩個查詢之間的「間隙時間」,這裏的鎖會讓全部其餘同一的查詢所有被阻塞。全部這樣的查詢將使用相同的索引,掃描索引相同結果的部分,因此極可能被阻塞。
      BEGIN;
      SELECT id FROM unsent_emails
          WHERE owner = 0 AND status = 'unsent'
          LIMIT 10 FOR UPDATE;
      -- result: 123, 456, 789
      UPDATE unsent_emails
          SET status = 'claimed', owner = CONNECTION_ID()
          WHERE id IN(123, 456, 789);
      COMMIT;
      -- 改進後更高效的寫法,無須使用SELECT查詢去找到哪些記錄尚未被處理。客戶端的協議會告訴你更新了幾條記錄,因此能夠直到此次須要處理多少條記錄。
      SET AUTOCOMMIT = 1;
      COMMIT;
      UPDATE unsent_emails
          SET status = 'claimed', owner = CONNECTION_ID()
          WHERE owner = 0 AND status = 'unsent'
          LIMIT 10;
      SET AUTOCOMMIT = 0;
      SELECT id FROM unsent_emails
          WHERE owner = CONNECTION_ID() AND status = 'claimed';
      -- result: 123, 456, 789複製代碼
  • 最後還需處理一種特殊狀況:那些正在被進程處理,而進程自己卻因爲某種緣由退出的狀況。

    • 只須要按期運行UPDATE語句將它都更新成原始狀態,而後執行SHOW PROCESSLIST,獲取當前正在工做的線程ID,並使用一些WHERE條件避免取到那些剛開始處理的進程

      -- 假設獲取的線程ID有(十、20、30),下面的更新語句會將處理時間超過10分鐘的記錄狀態更新成初始狀態。
      -- 將範圍條件放在WHERE條件的末尾,這個查詢剛好能勾使用索引的所有列,其它的查詢也都能使用上這個索引,這樣就避免了再新增一個額外的索引來知足其它的查詢
      UPDATE unsent_emails
          SET owner = 0, status = 'unsent'
        WHERE owner NOT IN(0, 10, 20, 30) AND status = 'cla
          AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;複製代碼

該案例中的一些基礎原則:

  • 儘可能少作事,能夠的話就不要作任何事。除非不得已,不然不要使用輪詢,由於這會增長負載,並且還會帶來不少低產出的工做。
  • 儘量快地完成須要作的事情。儘可能使用UPDATE代替先SELECT FOR UPDATE再UPDATE的寫法,由於事務的提交的速度越快,持有的鎖時間就越短,能夠大大減小競爭和加速串行執行效率。將已經處理完成和未處理的數據分開,保證數據集足夠小。
  • 這個案例的另外一個啓發是,某些查詢是沒法優化的;考慮使用不一樣的查詢或者不一樣的策略去實現相同的目的。一般對於SELECT FOR UPDATE就須要這樣的處理

有時,最好的辦法就是將任務隊列從數據庫中遷移出來,Redis和memcached就是一個很好的隊列容器。

6.8.2 計算兩點之間的距離

不建議使用MySQL作太複雜的空間計算存儲,PostgreSQL在這方面是一個不錯的選擇。一個典型的例子是計算以某個點爲中心,必定半徑內的全部點。例如查找某個點附近全部能夠出租的房子,或者社交網站中」匹配「附近的用戶。

假設咱們有以下表,這裏經度和緯度的單位都是度:

CREATE TABLE locations (
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(30),
  lat FLOAT NOT NULL,
  lon FLOAT NOT NULL
);
INSERT INTO locations(name, lat, lon)
  VALUES('Charlottesville, Virginia', 38.03, −78.48),
  ('Chicago, Illinois', 41.85, −87.65),
  ('Washington, DC', 38.89, −77.04);複製代碼

假設地球是圓的,而後使用兩點所在最大圓(半正矢)公式來計算兩點之間的距離。現有座標latA和lonA、latB和lonB,那麼點A和點B的距離計算公式以下:

ACOS(
COS(latA) * COS(latB) * COS(lonA - lonB)
+ SIN(latA) * SIN(latB)
)複製代碼

計算的結果是一個弧度,若是要將結果轉換成英里或公里,則須要乘以地球的半徑。

SELECT * FROM locations WHERE 3979 * ACOS(
  COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
  + SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;複製代碼

這類查詢不只沒法使用索引,並且還會很是消耗CPU時間,給服務器帶來很大的壓力,並且還得反覆計算。

優化地方:

  • 看看是否真的須要這麼精確的計算。其實該算法已經有不少不精確的地方:

    • 直線距離多是100英里,但實際上它們之間的行走距離極可能不是這個值。
    • 若是根據郵政編碼來肯定某我的所在的地區,再根據這個地區的中心位置計算他和別人的距離,這自己就是一個估算。
  • 若是不須要過高的精度,能夠認爲地球是圓的。要想有更多的優化,能夠將三角函數的計算放到應用中,而不要在數據庫中計算。

  • 看看是否真須要計算一個圓周,能夠考慮直接使用一個正方形代替。邊長爲200英里的正方形,一個頂點到中心的距離大概是141英里,這和實際計算的100英里相差並不太遠。根據正方形公式來計算弧度爲0.0253(100英里)的中心到邊長的距離:

    SELECT * FROM locations
      WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
      AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253);複製代碼

如今看看如何用索引來優化這個查詢:

  • 增長索引(lat, lon),這樣作的效果並不會很好,由於使用了範圍查詢。
  • 使用IN()優化。

新增兩個列,用來存儲座標的近似值FLOOR(),而後在查詢中使用IN()將全部點的整數值都放到列表中:

mysql> ALTER TABLE locations
-> ADD lat_floor INT NOT NULL DEFAULT 0,
-> ADD lon_floor INT NOT NULL DEFAULT 0,
-> ADD KEY(lat_floor, lon_floor);複製代碼

如今能夠根據座標的必定範圍的近似值來搜索,這個近似值包括地板值和天花板值,地理上分別對應的是南北:

-- 查詢某個範圍的全部點,數值須要在應用程序中計算而不是MySQL
mysql> SELECT FLOOR( 38.03 - DEGREES(0.0253)) AS lat_lb,
-> CEILING( 38.03 + DEGREES(0.0253)) AS lat_ub,
-> FLOOR(-78.48 - DEGREES(0.0253)) AS lon_lb,
-> CEILING(-78.48 + DEGREES(0.0253)) AS lon_ub;
+--------+--------+--------+--------+
| lat_lb | lat_ub | lon_lb | lon_ub |
+--------+--------+--------+--------+
| 36     | 40     | −80    | −77    |
+--------+--------+--------+--------+
-- 生成IN()列表中的整數:
SELECT * FROM locations
  WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
  AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253)
  AND lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77);複製代碼

使用近似值會讓咱們的計算結果有誤差,因此咱們還須要一些額外的條件過濾在正方形以外的點,這和前面使用CRC32作哈希索引相似:先建一個索引過濾出近似值,在使用精確條件匹配全部的記錄並移除不知足條件的記錄。

事實上,到這時就無須根據正方形的近似來過濾數據,可使用最大圓公式或者畢達哥拉斯定理來計算:

SELECT * FROM locations
  WHERE lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77)
  AND 3979 * ACOS(
  COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
  + SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;複製代碼

這時計算精度再次回到使用一個精確的圓周,不過如今的作法更快。只要可以高效地過濾掉大部分的點,例如使用近似整數和索引,以後再作精確數學計算的代價並不大。只要不是使用大圓周的算法,不然速度會更慢。

該案例使用的優化策略:

  • 儘可能少作事,可能的話儘可能不作事。這個案例中就不要對全部的點計算大圓周公式;先使用簡單的方案過濾大多數數據,而後再到過濾出來的更小的集合上使用複雜的公式運算。
  • 快速地完成事情。確保在設計中儘量地讓查詢都用上合適的索引,使用近似計算來避免複雜計算。
  • 須要的時候,儘量讓應用程序完成一些計算。

4.8.3 使用用戶自定義函數

  • 當SQL語句已經沒法高效地完成某些任務的時候,若是須要更快的速度,那麼C和C++是很好的選擇。
  • 案例:須要根據兩個隨機的64位數字計算它們的XOR值,來看這兩個數值是否匹配。大約有3500萬條的記錄須要在秒級中完成。
    • 通過簡單的計算就知道,當前的硬件條件下,不可能在MySQL中完成。
    • 編寫一個計算程序,以一個後臺程序的方式運行在同一服務器上,而後編寫一個用戶自定義函數,經過簡單的網絡通訊協議和前面的程序進行交互。分佈式運行該程序,能夠達到在130毫秒內完成4百萬次匹配計算。經過這樣的方式,能夠將密集型的計算放到一些通用服務器上,同時對外界徹底透明,看起來是MySQL完成了所有的工做。

4.9 總結

若是把建立高性能應用程序比做是一個環環相扣的」難題「,除了前面介紹的schema、索引和查詢語句設計以外,查詢優化應該是解開」難題「的最後一步。

理解查詢是如何被執行的以及時間都消耗在哪些地方,這依然是前面介紹的響應時間的一部分。再加上一些諸如解析和優化過程的知識,就能夠額更進一步地理解上一章討論的MySQL如何訪問表和索引的內容了。這也從另外一個維度理解MySQL在訪問表和索引時查詢和索引的關係。

優化一般須要三管齊下:不作、少作、快速地作。

相關文章
相關標籤/搜索