1. 程式人生 > 實用技巧 >MySql 資料庫查詢 優化

MySql 資料庫查詢 優化

自己在專案中,根據以下優化網頁內容,實際做的優化(紅字),後續進一步優化後再做更新,包括:

1.資料庫索引優化
2.慢SQL查詢優化
分解SQL關聯查詢
切分複雜SQL查詢
優化查詢SQL資料訪問
3.mysql讀寫分離
4.mysql配置優化

1.為什麼查詢速度為變慢

  在嘗試編寫快速的查詢之前,需要清楚一點,真正重要是響應時間。如果把查詢看作是一個任務,那麼他由一系列子任務組成,每個子任務都會消耗一定的時間。如果要優化查詢,實際上要優化其子任務,要麼消除其中一些子任務,要麼減少子任務的執行的次數,要麼讓子任務執行得更快。

  MySQL在執行查詢的時候有哪些子任務。哪些子任務執行的速度很慢,這裡很難給出完整的列表,通常來說查詢的生命週期大致可以按照順序來看:從客戶端,到伺服器,然後再伺服器上進行解析,生成執行計劃,執行,並返回結果給客戶端。其中“執行”可以認為是整個生命週期中最重要的階段,這其中包括了大量為了檢索資料到儲存引擎的呼叫以及呼叫後的資料處理,包括排序、分組等。  

  在完成這些任務的時候,查詢需要在不同的地方花費時間,包括網路,CPU計算,生成統計資訊和執行計劃、鎖等待(互斥等待)等操作,尤其是向底層儲存引擎檢索資料的呼叫操作,這些呼叫需要在記憶體操作、CPU操作和記憶體不足時導致的I/O操作上消耗時間,根據引擎不同,可能還會產生大量的上下文切換以及系統呼叫。

  在每一個消耗大量時間的查詢案例中,我們都能看到一些不必要的額外操作、某些操作被額外地重複了很多次、某些操作執行得太慢等。優化查詢的目的就是減少和消除這些操作所花費的時間。

2.慢查詢基礎:優化資料庫訪問

  2.1 是否想伺服器請求了不需要的資料:請求過量的資料或者重複請求資料

  2.2 MYSQL是否在掃描額外的記錄

    最簡單衡量查詢開銷的三個指標

    • 響應時間:分為服務時間和排隊時間

            服務時間:是指資料庫處理這個查詢真正花了多長時間

            排隊時間:是指伺服器因為等待某些資源而沒有真正執行查詢的時間——坑內是等I/O操作完成,也可能使行鎖等等

    • 掃描的行數
    • 返回的行數 

          在EXPLAIN語句中的type列反應了訪問的型別。訪問型別有很多種,從全表掃描到索引掃描、範圍掃描、唯一索引查詢、常數引用等。這裡列的這些,速度是從慢到快,掃描的行數也是小到大。你不需要記住這些訪問型別,但是要明白掃描表,掃描索引,範圍訪問和單值訪問的概念。如果查詢沒有辦法找到合適的訪問型別,那麼最好的辦法通常就是增加一個合適的索引。

    一般MySQL能夠使用如下三種應用WHERE條件,從好到壞依次為:

    • 在索引中使用WHERE條件來過濾不匹配的記錄。這是在儲存引擎層完成的。
    • 使用索引覆蓋掃描來返回記錄,直接從索引中過濾不需要的記錄並返回命中的結果。這是在MySQL伺服器層完成的,但無須在回表查詢記錄。
    • 從資料表中返回資料,然後過濾不滿足條件的記錄。這是在MySQL伺服器層完成,MySQL需要先從資料表讀出記錄然後過濾。

    如果說發現查詢需要掃描大量的資料但只返回少數的行,那麼通常可以嘗試下面的技巧去優化它:

    • 使用索引覆蓋掃描,把所有需要用到的列都放到索引中,這樣儲存引擎無須回表獲取對應行就可以返回結果
    • 改變庫表結構。例如使用單獨的彙總表
    • 重寫這個複雜的查詢,讓MySQL優化器能夠以更優化的方式執行這個查詢 

3.重構查詢方式

  3.1 一個複雜查詢還是多個簡單查詢

  3.2 切分查詢:將大查詢切分成小查詢,每個查詢完全一樣,只完成一小部分,每次只返回一小部分查詢結果。

  3.3 分解關聯查詢

      對每一個表進行一次單表查詢,然後再應用程式中進行關聯,例如

1

2

3

4

mysql>SELECT*FROMtag

   ->JOINtag_postONtag_post.tag_id=tag.id

  ->JOINpostONtag_post.post_id=post.id

  ->WHEREtag.tag='mysql';

      可以分解成下面的語句來代替  

1

2

mysql>SELECT*FROMtagWHEREtag='mysql';<br>   mysql>SELECT*FROMtag_postWHEREtag_id=1234;

mysql>SELECT*FROMpostWHEREpost.idin(123,456,567,9098,8904);

      使用分解關聯查詢的方式重構查詢有如下的優化:

      • 讓快取的效率更高。
      • 將查詢分解後,執行單個查詢可以減少鎖的競爭。
      • 在應用層做關聯,可以更容易的對資料庫進行拆分,更容易做到高效能和可擴充套件。
      • 查詢本身效率也可能會有所提升。
      • 可以減少冗餘記錄的查詢。
      • 這樣做相當於在應用中實現了雜湊關聯,而不是使用MySQL的巢狀迴圈關聯。     

4.查詢執行的基礎

    查詢執行路徑

    

     步驟:

        (1)客服端傳送一條查詢給伺服器

        (2)伺服器先檢查查詢快取,如果命中快取,則立刻返回儲存在快取中的結果。否則進入下一個階段。

        (3)伺服器端進行SQL解析、預處理,在由優化器生成對應的執行計劃。

        (4)MySQL根據優化器生成的執行計劃,呼叫儲存引擎的API來執行查詢

        (5)將結果返回給客戶端

  4.1 MySQl客戶端/伺服器通訊協議  

    (1)MySQL客戶端和伺服器之間的通訊是”雙半工“的,這意味著,在任何一個時刻,要麼是由伺服器向客戶端傳送資料,要麼是由客戶端向伺服器傳送資料,這兩個動作不能同時發生。

    (2)查詢狀態:對於一個MySQL連線,或者說一個執行緒,任何時刻都有一個狀態,表示MySQL當前在做什麼。我們使用最簡單的SHOW FULL PROCESSLIST命令(該命令返回結果中的Command列就表示當前的狀態)來查詢。下面將這些狀態列出來,並做一個簡單的解釋: 

      a. Sleep:執行緒正在等待客戶端傳送新的請求。

  b. Query:執行緒正在執行查詢或者正在將結果傳送給客戶端。

  c. Locked:在MySQL伺服器層,該執行緒正在等待表鎖。

  d. Analyzing and statistics : 執行緒正在收集儲存引擎的統計資訊,並生成查詢的執行計劃。

  e. Coping to tmp table [on disk]:執行緒正在執行查詢,並且將其結果都複製到一個臨時表中,這種狀態一般要麼是在做GROUP BY操作,要麼是檔案排序操作,或者是UNION操作。如果這個狀態後面

  還有"on disk"標記,那表示MySQL正在講一個記憶體臨時表放到磁碟上。

  f. Sorting result:執行緒正在對結果集進行排序。

  g. Sending data:這表示多種情況:執行緒可能在對多個狀態之間傳輸資料,或者而在生成結果集,或者在向客戶端返回資料。

 

  4.2 查詢快取

    (1)在解析一個查詢語句之前,如果查詢快取是開啟的,那麼MYSQL會優先檢查這個查詢是否命中查詢快取中的資料。

    (2)這個檢查是通過一個對大小寫敏感的雜湊查詢的。查詢和快取中的查詢即使只有一個不同,也不會匹配快取結果。

    (3)如果命中快取,那麼在但會結果前MySQL會檢查一次使用者許可權,有許可權則跳過其他步驟直接返回資料

  4.3 查詢優化處理

      查詢的生命週期的下一步是將一個SQL轉換成執行計劃,MySQL再依照這個執行計劃和儲存引擎進行互動。

    4.3.1 語法解析器和預處理

       MySQL解析器將使用MySQL語法規則驗證和解析查詢。例如驗證是否使用錯誤的關鍵字、關鍵字順序、引號前後是否匹配等

       前處理器則根據一些MySQL 規則進一步解析樹是否合法,例如檢查資料表和資料列是否存在,解析名字和別名是否有歧義等

    4.3.2 查詢優化器

       一條查詢可以有很多種執行方式,最後都返回相同的結果。優化器的作用就是找到其中最好的執行計劃

       有很多中原因導致MySQL優化器選擇錯誤的計劃,如下所示:

      • 統計資訊不準確:MySQL依賴儲存引擎提供的統計資訊來評估成本,但是有的儲存引擎提供的資訊偏差有點大,例如InnoDB因為其MVCC的架構,並不能維護一個數據表的行數的精確統計資訊
      • 執行計劃中的成本估算不等於實際的操作成本
      • MySQL的最優可能和你想的最優不一樣
      • MySQL從不考慮其他併發執行的查詢
      • MySQL也並不是任何時候都是基於成本的優化
      • MySQL不會考慮不受其控制的操作成本。例如執行儲存過程或者使用者自定義函式的成本
      • 優化器有時間無法估算所有可能的執行計劃

      MySQL的查詢優化器使用很多策略來生成一個最優的執行計劃。

      優化策略可以簡單的分為兩種

      靜態優化: 靜態優化可以直接對解析樹進行分析,並完成優化。例如優化器可以通過簡單的代數變化將WHERE條件轉換成另外一種等價形式,靜態優化在第一次完成後就一直有效,即使使用不同的引數重複執行查詢也不會變化。可以認為是一種”編譯時優化“

      動態優化:和查詢的上下文有關,也可能和其他因素有關,例如WHERE中取值、索引中條目對應的資料行數等。這需要在每次查詢的時候重新評估,可以讓那位u是”執行時優化“。

      MySQL能夠處理的優化型別:(部分)

      • 重新定義關聯表順序
      • 將外連線轉化成內連線
      • 使用等價變換規則
      • 優化COUNT() 、MIN() 、 MAX()
      • 預估並轉換為常數表示式
      • 覆蓋索引掃描
      • 子查詢優化
      • 提前終止查詢
      • 等值傳播
      • 列表IN()的比較

    4.3.3 資料和索引的統計資訊

      在伺服器層有查詢優化器,卻沒有儲存資料和索引的統計資訊。統計資訊由儲存引擎實現,不同的儲存引擎可能會儲存不同的統計資訊,有的引擎根本不儲存任何統計資訊,例如Archive引擎。

      因為伺服器層沒有任何統計資訊,所有MySQL查詢優化器在生成查詢的執行計劃時,需要向儲存引擎獲取相應的統計資訊,優化器根據這些資訊來選擇一個最優的執行計劃。

    4.3.4 MySQL如何執行關聯查詢

        MySQL中“關聯”認為任何一個查詢都是一次“關聯”,並不僅僅是一個查詢需要到兩個表匹配才叫關聯。素以在MySQL中,每一個查詢,每一個片段(包括子查詢,甚至於單表的SELECT)都可能是關聯。

        MySQL關聯查詢的策略很簡單:MySQL對任何關聯都執行巢狀迴圈關聯操作,即MySQL先在要給表中迴圈取出單條資料,然後再巢狀迴圈到下一個表中尋找匹配的行,依次下去,直到找到所有表中匹配的行為止。然後根據各個表的行,返回查詢中需要的各個列。

    4.3.5 執行計劃

      和很多其他關係資料庫不同,MySQL並不會生成查詢位元組碼來執行查詢。MySQL生成查詢的一顆指令樹,然後通過儲存引擎執行完成這顆樹並返回結果

    4.3.6 關聯查詢優化器

      如果優化器給出的並不是最優的關聯順序,這時可以使用STRAIGHT_JOIN關鍵字重寫查詢,讓優化器按照你認為最優的關聯順序執行——不過老實說,人的判斷很難那麼精準。絕大多數時候,優化器做出的選擇都比普通人的判斷更精準。

      如果超過N個表的關聯,那麼需要檢查N的階乘種關聯順序。我們稱之為所有可能的執行計劃的“搜尋空間‘,當搜尋空間非常大的時候,優化器選擇使用”貪婪“搜尋方式查詢”最優’的關聯順序。當關聯的表超過optimizer_search_depth的限制的時候,就會選擇“貪婪”搜尋模式了。

    4.3.7 排序優化   

      排序優化:無論如何排序都是一個成本很高的操作,所以從效能角度考慮,應儘可能避免排序或者儘可能避免對大量資料進行排序。儘量通過索引進行排序。當不能使用索引生成排序結果的時候,MySQL需要自己

  進行排序,如果資料量小則在記憶體中進行,如果數量大則需要使用磁碟,不過MySQL將這個過程統一稱為檔案排序,即使完全是記憶體排序不需要任何磁碟檔案時也是如此。

   MySQL有如下兩種排序演算法:

  a. 兩次傳輸排序(舊版本使用):讀取行指標和需要排序的欄位,對其進行排序,然後再根據排序結果讀取所需要的資料行。需要進行兩次傳輸,即需要從資料表中讀取兩次資料,第二次讀取資料的時候,因為是讀

   取排序列進行排序後的所有記錄。這回產生大量的隨機IO。

  b. 單次傳輸排序(新版本使用):先讀取查詢所需要的所有列,然後在根據給定列進行排序,最後直接返回排序結果。效率更高,但佔用記憶體更大。

  如果查詢中有LIMIT的話,LIMIT也會在排序之後應用的,所以即使需要返回較少的資料,臨時表和需要排序的資料量仍然後非常大。貌似5.6版本有所改進,會先拋棄不滿足條件的記錄,然後再進行排序。

  4.4 查詢執行引擎 

    在解析和優化階段,MySQL將生成查詢對應的執行計劃,MySQL的查詢執行引擎則根據這個執行計劃來完成整個查詢。這裡執行計劃是一個數據結構,而不是和很多其他的關係型資料庫那樣會生成對應的位元組碼。

  4.5 返回結果給客戶端

    即使查詢不需要返回結果集給客戶端,MySQL仍然會返回這個查詢的一些資訊,如查詢影響到的行數。如果查詢可以被快取,那麼MySQL在這個階段也會將結果存放到快取中。

    MySQL將結果集返回客戶端是一個增量、逐步返回的過程。開始生成第一條結果時,MySQL就開始向客戶端逐步返回結果集了。

   

5. MySQL查詢優化器的侷限性:

5.1 關聯子查詢:MySQL的子查詢實現非常糟糕(5.6版本以後有改進)最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。

1). 因為使用IN()加子查詢,效能經常會非常糟,所以通常建議使用EXISTS()等效的改寫查詢來獲取更好的效率。

2). 一般建議使用左外連線(LEFT OUTER JOIN)代替子查詢(?)。

5.2 UNION的限制:MySQL無法將限制條件從外層"下推"到內層,這使得原本能夠限制部分返回結果的條件無法應用到內層查詢的優化上。

例如如果希望UNION的各個子句能夠根據LIMIT只去部分結果集,或者希望能夠先排好序再合併結果集的話,就需要在UNION的各個子句中分別使用這些語句。

(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;

優化後:

(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;

5.3 當WHERE子句包含多個複雜條件的時候,MySQL能夠訪問單個表的多個索引以合併和交叉過濾的方式來定位需要查詢的行。

5.4 等值查詢:某些時候,等值查詢會帶來一些意想不到額外消耗。例如:有一個非常大的IN()列表,而MySQ優化器發現存在WHERE、ON或者USING的子句。

5.5 並行執行:MySQL無法利用多核特性來並行執行查詢(貌似5.6以後有改進)。

5.6 雜湊關聯:MySQL不支援雜湊關聯。

5.7 鬆散索引掃描:MySQL並不支援鬆散索引掃描,也就無法按照不連續的方式掃描一個索引。通常,MySQL的索引掃描需要先定義一個起點和終點,即使需要的資料只是這段索引中的很少幾個,MySQL仍需掃描這段索引中

的每一個條目。

5.8 最大值和最小值優化:對於MIN()和MAX()查詢,MySQL的優化做的並不好。例如:

SELECT MIN(actor_id) FROM sakila.actor WHERE first_name='PENELOPE'

因為first_name上沒有索引,所以會進行全表掃描。如果MySQL能夠進行主鍵掃描,那麼理論上,當MySQL讀到第一個滿足條件的記錄的時候,就是我們需要的最小值,因為主鍵是嚴格按照actor_id大小欄位排序的。

一個曲線優化的辦法是移除MIN(),然後使用LIMIT來將查詢重寫。

5.9 在同一個表上查詢和更新:MySQL不允許對同一張表同時進行查詢和更新。

6. 查詢優化器的提示(hint):

  如果對優化器選擇的執行計劃不滿意,可以使用優化器提供的幾個提示(hint)來控制最終的執行計劃。

7. 優化特定型別的查詢

7.1 優化COUNT()查詢

1). COUNT()是一個特殊的函式,有兩種非常不同的作用:它可以統計某個列值的數量,也可以統計行數。在統計列值的時候要求列值是非空的(不統計NULL)。如果COUNT()的括號中指定了列或者列的表示式,則

統計的就是這個表示式有值的結果數。最簡單的就是我們使用count(*)的時候,這種情況下萬用字元*並不會向我們猜想的那樣擴充套件所有的行,實際上,它會忽略所有的值而直接統計所有的行數。

2). 使用近似值:有時候某些業務場景並不要求完全精確的COUNT值,此時可以用近似值來代替。

3). 更復雜的優化:覆蓋索引,增加彙總表等。

7.2 優化關聯查詢:

1). 確保ON或者USING子句中的列上有索引。在建立索引的時候就要考慮到關聯的順序。當表A和表B用到列C關聯的時候,如果優化器關聯順序是B、A,那就不需要在B表的對應列上建立索引。沒有用到的索引只會

帶來額外的負擔。一般來說,除非有其他理由,否則只需要在關聯順序中的第二個表的相應列上建立索引。

2). 確保任何的GROUP BY 和ORDER BY中的表示式只涉及到一個表中的列。這樣MySQL才有可能使用索引來優化這個過程。

7.3 優化子查詢:關於優化子查詢我們給出的最重要的優化建議就是儘可能使用關聯查詢代替,至少當前MySQL版本需要這樣。

7.4 優化GROUP BY和DISTINCT:

1). 它們都可以使用索引來優化,這也是最有效的方法。

2). 在MySQL中,當無法使用索引的時候,GROUP BY使用兩種策略來完成:使用臨時表或檔案排序來做分組。對於任何查詢語句,這兩種策略的效能都有可以提升的地方。可以通過使用提示SQL_BIG_RESULT和

SQL_SMALL_RESULT來讓優化器按你希望的方式執行。

3). 如果需要對關聯查詢分組(GROUP BY),並且是按照查詢表中的某個列進行分組,那麼通常採用查詢表的標識列分組的效率比其他列更高。

4). 如果沒有通過ORDER BY子句顯式地指定排序列,當查詢使用GROUP BY 子句的時候,結果集會自動按照分組的列進行排序。如果不關心結果集的順序,而這中預設排序又導致了需要檔案排序,則可以使用

ORDER BY NULL,讓MySQL檔案不再進行排序。也可以在GROUP BY子句中直接使用DESC或者ASC關鍵字,使分組的結果集按照需要的方向排序。

5). 優化GROUP BY WITH ROLLUP:分組查詢的一個變種思想就是要求MySQL對返回的分組結果再做一次超級聚合。最好的辦法儘可能的將WITH ROLLUP 功能轉移到應用程式中處理。

7.5 優化LIMIT分頁:

1). 使用索引

2). 要優化這種查詢,要麼是在頁面中限制分頁的數量,要麼是優化大偏移量的效能。

3). 盡肯能的使用索引覆蓋

4). 延遲關聯

5). 有時候也可以將LIMIT查詢轉換為已知位置的查詢,讓MySQL通過範圍掃描找到對應的結果。

6). 其他優化辦法還包括使用預先計算的彙總表,或者關聯一個冗餘表,冗餘表只包含主鍵列和需要做排序的資料列。

7.6 優化SQL_CALC_FOUND_ROWS:分頁的時候,另一個常用的技巧是在LIMIT語句中加上SQL_CALC_FOUND_ROWS提示(hint),這樣就可以獲得去掉LIMIT以滿足條件的行數,因此可以作為分頁的總數。

用業務的手段解決:下一頁,獲取更多資料等。

7.7 優化UNION查詢:

1). MySQL總是通過建立填充臨時表的方式來執行UNION查詢。因此很多優化策略在UNION查詢中都沒法很好地使用。經常需要手工地將WHERE,LIMIT,ORDER BY等子句"下推"到UNION的各個子查詢中,以

便優化器可以充分利用這些條件進行優化。

2). 除非確實需要伺服器消除重複的行,否則就一定要使用UNION ALL,這一點很重要。如果沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這回導致對臨時表做唯一性檢查。這樣做的代價非常高,

即使有ALL關鍵字,MySQL仍然會使用臨時表儲存結果。事實上,MySQL總是經結果放入臨時表,然後再讀出,再返回給客戶端。

7.8 靜態查詢分析:Percona Toolkit中的pt-query-advisor 能夠解析查詢日誌、分析查詢模式,然後再給出所有可能存在的潛在問題的查詢,並給出足夠詳細的建議。這像是給MySQL所有的查詢做一次全面的健康

檢查,它能檢測出很多問題。

7.9 使用者自定義變數