1. 程式人生 > 資料庫 >深入淺出mysql優化--一篇部落格讓你精通mysql優化策略--中

深入淺出mysql優化--一篇部落格讓你精通mysql優化策略--中

7. mysql 優化case

  • 1.Order by與Group by優化

      1. explain select * from employees where name = 'LiLei' and position = 'dev' order by age;
      
       利用最左字首法則:中間欄位不能斷,因此查詢用到了name索引,從key_len=98也能看出,age索引列用在排序過程中,
       因為Extra欄位裡沒有using filesort
      
      
      2. explain select * from employees where name = 'LiLei' order by position;
       從explain的執行結果來看:key_len=98,查詢使用了name索引,由於用了position進行排序,跳過了 age,出現了Using filesort
      
      
      3. explain select * from employees where name = 'LiLei' order by age,position;
       查詢只用到索引name,age和position用於排序,無Using filesort
    

    圖25

      4. explain select * from employees where name = 'LiLei' order by position,age;
       和Case 3中explain的執行結果一樣,但是出現了Using filesort,
       因為索引的建立順序為name,age,position,但是排序的時候age和position顛倒位置了。
       
      5. explain select * from employees where name = 'LiLei' order by age asc,position desc;
        排序的欄位列與索引順序一樣,且 order by 預設升序,這裡position desc變成了降序,
        導致與索引的排序方式不同,從而產生Using filesort。
        Mysql8以上版本有降序索引可以支援該種查詢方式
        
      6. explain select * from employees where name in ('LiLei','zhaqngsan') order by age,position;
        對於排序來說,多個相等條件也是範圍查詢
      
      7. explain select * from employees where name > 'a' order by name;
         可以用覆蓋索引優化
         explain select name,age,position from employees where name > 'a' order by name;
    

圖26

    總結
        1、MySQL支援兩種方式的排序filesort和index,Using index是指MySQL掃描索引本身完成排序。index效率高,filesort效率低。
        2、order by滿足兩種情況會使用Using index。
            1) order by語句使用索引最左前列。
            2) 使用where子句與order by子句條件列組合滿足索引最左前列。
        3、儘量在索引列上完成排序,遵循索引建立(索引建立的順序)時的最左字首法則。
        4、如果order by的條件不在索引列上,就會產生Using filesort。
        5、能用覆蓋索引儘量用覆蓋索引
        6、group by與order by很類似,其實質是先排序後分組,遵照索引建立順序的最左字首法則。
           對於group by的優化如果不需要排序的可以加上order by null禁止排序。注意,where高於having,
           能寫在where中的限定條件就不要去having限定了。
  • Using filesort檔案排序原理詳解

     filesort檔案排序方式
         單路排序:
             是一次性取出滿足條件行的所有欄位,然後在sort buffer中進行排序;
             用trace工具可以看到sort_mode資訊裡顯示< sort_key, additional_fields >或者< sort_key,packed_additional_fields >
             
         雙路排序(又叫回表排序模式):
             是首先根據相應的條件取出相應的排序欄位和可以直接定位行資料的行 ID,然後在sort buffer中進行排序,
             排序完後需要再次取回其它需要的欄位;用trace工具可以看到sort_mode資訊裡顯示< sort_key, rowid >
             
     MySQL通過比較系統變數 max_length_for_sort_data(預設1024位元組) 的大小和需要查詢的欄位總大小來判斷使用哪種排序模式
         如果 max_length_for_sort_data 比查詢欄位的總長度大,那麼使用 單路排序模式;
         如果 max_length_for_sort_data 比查詢欄位的總長度小,那麼使用 雙路排序模式。
     
     示例驗證下各種排序方式:
         explain select * from employees where name = 'zhangsan' order by position;
         
         
         
     單路排序的詳細過程:
         1. 從索引name找到第一個滿足name = ‘zhuge’條件的主鍵id
         2. 根據主鍵id取出整行,取出所有欄位的值,存入sort_buffer中
         3. 從索引name找到下一個滿足name = ‘zhuge’條件的主鍵id
         4. 重複步驟 2、3 直到不滿足name = ‘zhuge’
         5. 對sort_buffer中的資料按照欄位position進行排序
         6. 返回結果給客戶端
         
     雙路排序的詳細過程
         1. 從索引name找到第一個滿足name = ‘zhuge’的主鍵id
         2. 根據主鍵id取出整行,把排序欄位position和主鍵id這兩個欄位放到sort buffer中
         3. 從索引name取下一個滿足name = ‘zhuge’記錄的主鍵id
         4. 重複3、4直到不滿足name = ‘zhuge’
         5. 對sort_buffer中的欄位position和主鍵id按照欄位position進行排序
         6. 遍歷排序好的id和欄位position,按照id的值回到原表中取出所有欄位的值返回給客戶端
         
     其實對比兩個排序模式,單路排序會把所有需要查詢的欄位都放到sort buffer中,而雙路排序只會把主鍵和需要排序的欄位放到 sort buffer中進行排序,
     然後再通過主鍵回到原表查詢需要的欄位。
     如果MySQL排序記憶體配置的比較小並且沒有條件繼續增加了,可以適當把 max_length_for_sort_data 配置小點,
     讓優化器選擇使用雙路排序演算法,可以在sort_buffer中一次排序更多的行,只是需要再根據主鍵回到原表取資料。
     如果MySQL排序記憶體有條件可以配置比較大,可以適當增大max_length_for_sort_data的值,讓優化器優先選擇全欄位排序(單路排序),
     把需要的欄位放到sort_buffer中,這樣排序後就會直接從記憶體裡返回查詢結果了。
     所以,MySQL通過 max_length_for_sort_data 這個引數來控制排序,在不同場景使用不同的排序模式,從而提升排序效率。
     
     注意:
         如果全部使用sort_buffer記憶體排序一般情況下效率會高於磁碟檔案排序,
         但不能因為這個就隨便增大sort_buffer(預設1M),mysql很多引數設定都是做過優化的,不要輕易調整
    
  • 分頁查詢的優化

     create procedure insert_emp()
     begin
     declare i int;
     set i =1;
     while(i<=100000)do
     insert into employees(name,age,position) values(CONCAT('zhuge',i),i,'dev');
         set i=i+1;
     end while;
     end;
     call insert_emp();
    

    很多時候我們業務系統實現分頁功能可能會用如下sql實現

     select * from employees limit 10000,10;
     表示從表 employees中取出從10001行開始的10行記錄。看似只查詢了10條記錄,
     實際這條SQL是先讀取10010條記錄,然後拋棄前10000條記錄,然後讀到後面10條想要的資料。
     因此要查詢一張大表比較靠後的資料,執行效率是非常低的。
    
  • 常見的分頁場景優化

     1、根據自增且連續的主鍵排序的分頁查詢
         select * from employees limit 90000,5;
       該 SQL 表示查詢從第 90001開始的五行資料,沒新增單獨 order by,表示通過主鍵排序。
       再看錶employees,因主鍵是自增並且連續的,所以可以改寫成按照主鍵去查詢從第 90001開始的五行資料,如下:
         select * from employees where id > 90000 limit 5;
       改寫後的 SQL 走了索引,而且掃描的行數大大減少,執行效率更高。
       但是,這條 改寫的SQL 在很多場景並不實用,因為表中可能某些記錄被刪後,主鍵空缺,導致結果不一致,
       如果主鍵不連續,不能使用上面描述的優化方法。
       另外如果原 SQL 是 order by 非主鍵的欄位,按照上面說的方法改寫會導致兩條SQL的結果不一致。所
       以這種改寫得滿足以下兩個條件:
           主鍵自增且連續
           結果是按照主鍵排序的
           
     2、根據非主鍵欄位排序的分頁查詢
         explain select* from employees order by name limit 90000,5;
    
         發現並沒有使用name欄位的索引(key欄位對應的值為 null),
         具體原因:
         
             掃描整個索引並查詢到沒索引的行(可能要遍歷多個索引樹)的成本比掃描全表的成本更高,所以優化器放棄使用索引。
             知道不走索引的原因,那麼怎麼優化呢?
             其實關鍵是讓排序時返回的欄位儘可能少,所以可以讓排序和分頁操作先查出主鍵,然後根據主鍵查到對應的記錄,SQL改寫如下
        
             explain select* from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
             需要的結果與原 SQL 一致,執行時間減少了一半以上,我們再對比優化前後sql的執行計劃:
             原 SQL 使用的是 filesort 排序,而優化後的 SQL 使用的是索引排序。
    

圖27

  • join 關聯查詢優化

     create table t1(
      id int(11) not null auto_increment,
      a int(11) default null,
      b int(11) default null,
      primary key(id),
      key `idx_a` (a)
     )ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
     
     create table t2 like t1;
     
     往t1表插入1萬行記錄,往t2表插入100行記錄
         create procedure insert_emp_t1()
         begin
         declare i int;
         set i =1;
         while(i<=10000)do
         insert into t1(a,b) values(i,i+1);
             set i=i+1;
         end while;
         end;
         call insert_emp_t1();
         
         create procedure insert_emp_t2()
         begin
         declare i int;
         set i =1;
         while(i<=100)do
         insert into t2(a,b) values(i,i+1);
             set i=i+1;
         end while;
         end;
         call insert_emp_t2();
    
  • mysql的表關聯常見有兩種演算法

     Nested-Loop Join 演算法
     Block Nested-Loop Join 演算法
     
     1、巢狀迴圈連線 Nested-Loop Join(NLJ) 演算法
     
        一次一行迴圈地從第一張表(稱為驅動表)中讀取行,在這行資料中取到關聯欄位,
        根據關聯欄位在另一張表(被驅動表)裡取出滿足條件的行,然後取出兩張表的結果合集。
        
        explain select * from t1 inner join t2 on t1.a = t2.a;
        
        從執行計劃中可以看到這些資訊:
            驅動表是t2,被驅動表是t1。
            先執行的就是驅動表(執行計劃結果的id如果一樣則按從上到下順序執行sql);
            優化器一般會優先選擇小表做驅動表。所以使用inner join時,排在前面的表並不一定就是驅動表。
            使用了NLJ演算法。一般join語句中,如果執行計劃Extra中未出現Using join buffer則表示使用的join演算法是NLJ。
        
        上面sql的大致流程如下:
            1. 從表t2中讀取一行資料;
            2. 從第1步的資料中,取出關聯欄位a,到表t1中查詢;
            3. 取出表t1中滿足條件的行,跟t2中獲取到的結果合併,作為結果返回給客戶端;
            4. 重複上面3步。
            
        整個過程會讀取t2表的所有資料(掃描100行),然後遍歷這每行資料中欄位a的值,根據t2表中a的值索引掃描t1表中的對應行
        (掃描100次t1表的索引,1次掃描可以認為最終只掃描t1表一行完整資料,也就是總共 t1 表也掃描了100行)。因此整個過程掃描了200行。
        如果被驅動表的關聯欄位沒索引,使用NLJ演算法效能會比較低(下面有詳細解釋),mysql會選擇Block Nested-Loop Join演算法。
    

Index Nested-Loop Join 演算法的執行流程圖

    2、基於塊的巢狀迴圈連線 Block Nested-Loop Join( BNL )演算法
        
       把驅動表的資料讀入到 join_buffer中,然後掃描被驅動表,把被驅動表每一行取出來跟 join_buffer 中的資料做對比。
       
       explain select * from t1 inner join t2 on t1.b = t2.b
       
       Extra中的Using join buffer(Block Nested Loop)說明該關聯查詢使用的是BNL演算法。
       
       上面sql的大致流程如下:
           1. 把t2的所有資料放入到join_buffer中
           2. 把表t1中每一行取出來,跟join_buffer中的資料做對比
           3. 返回滿足join條件的資料
       整個過程對錶t1和t2都做了一次全表掃描,
       因此掃描的總行數為10000(表t1的資料總量)+100(表t2的資料總量) =10100。
       並且join_buffer裡的資料是無序的,因此對錶t1中的每一行,都要做100次判斷,所以記憶體中的判斷次數是100*10000=100萬次。

Block Nested-Loop Join 演算法的執行流程

       被驅動表的關聯欄位沒索引為什麼要選擇使用BNL演算法而不使用Nested-Loop Join呢?
           假設小表的行數是 N,大表的行數是 M,那麼在這個演算法裡:
           1. 兩個表都做一次全表掃描,所以總的掃描行數是 M+N;
           2. 記憶體中的判斷次數是 M*N。
           可以看到,調換這兩個算式中的 M 和 N 沒差別,因此這時候選擇大表還是小表做驅動表,執行耗時是一樣的
           
           如果上面第二條sql使用Nested-Loop Join,那麼掃描行數為100 * 10000 = 100萬次,這個是磁碟掃描。
           很顯然,用BNL磁碟掃描次數少很多,相比於磁碟掃描,BNL的記憶體計算會快得多。
           因此MySQL對於被驅動表的關聯欄位沒索引的關聯查詢,一般都會使用BNL演算法。如果有索引一般選擇NLJ演算法,有索引的情況下NLJ演算法比BNL演算法效能更高
        
        如果join_buffer放不下怎麼辦?
        join_buffer 的大小是由引數 join_buffer_size 設定的,預設值是 256k。如果放不下表 t2的所有資料話,策略很簡單,就是分段放,
        假如把join_buffer_size設定大一點再次執行,過程就會變成如下
            1. 掃描表 t2,順序讀取資料行放入 join_buffer 中,放完第 88 行 join_buffer 滿了,繼續第 2 步;
            2. 掃描表 t1,把 t1 中的每一行取出來,跟 join_buffer 中的資料做對比,滿足 join 條件的,作為結果集的一部分返回;
            3. 清空 join_buffer;
            4. 繼續掃描表 t2,順序讀取最後的 12行資料放入 join_buffer 中,繼續執行第 2 步。

Block Nested-Loop Join -- 兩段圖

          可以看到,這時候由於表t2被分成了兩次放入 join_buffer 中,導致表 t1 會被掃描兩次。
          雖然分成兩次放入join_buffer,但是判斷等值條件的次數還是不變的,依然是(88+12)*1000=10 萬次
        
       對於關聯sql的優化
           關聯欄位加索引,讓mysql做join操作時儘量選擇NLJ演算法
           小標驅動大表,寫多表連線sql時如果明確知道哪張表是小表可以用straight_join寫法固定連線驅動方式,省去mysql優化器自己判斷的時間
       
       
       straight_join解釋:
        straight_join功能同join類似,但能讓左邊的表來驅動右邊的表,能改表優化器對於聯表查詢的執    行順序。
       比如:
         explain select * from t2 straight_join t1 on t2.a = t1.a;
         代表制定mysql選著 t2 表作為驅動表。
       straight_join只適用於inner join,並不適用於left join,right join。
       (因為left join,right join已經代表指定了表的執行順序)
       儘可能讓優化器去判斷,因為大部分情況下mysql優化器是比人要聰明的。
       使用straight_join一定要慎重,因為部分情況下人為指定的執行順序並不一定會比優化引擎要靠譜。
       
    問題:
        能不能使用 join?
            假設不使用 join,那我們就只能用單表查詢。我們看看上面這條語句的需求,用單表查詢實現
            1. 執行select * from t2,查出表 t1 的所有資料,這裡有 100 行;
            2. 迴圈遍歷這 100 行資料:
            可以看到,在這個查詢過程,也是掃描了200行,但是總共執行了101條語句,比直接join 多了 100 次互動。
            除此之外,客戶端還要自己拼接 SQL 語句和結果。顯然,這麼做還不如直接 join好
            
            1. 如果可以使用 Index Nested-Loop Join 演算法,也就是說可以用上被驅動表上的索引,其實是沒問題的;
            2. 如果使用 Block Nested-Loop Join 演算法,掃描行數就會過多。尤其是在大表上的 join操作,
               這樣可能要掃描被驅動表很多次,會佔用大量的系統資源。所以這種 join 儘量不要用。
            在判斷要不要使用 join 語句時,就是看 explain 結果裡面,Extra 欄位裡面有沒有出現“Block Nested Loop”字樣
            
        怎麼選擇驅動表?
            在這個 join語句執行過程中,驅動表是走全表掃描,而被驅動表是走樹搜尋。
            假設被驅動表的行數是 M。每次在被驅動表查一行資料,要先搜尋索引 a,再搜尋主鍵索引。
            每次搜尋一棵樹近似複雜度是以 2為底的 M 的對數,記為 log M,所以在被驅動表上查一行的時間複雜度是 2*log M。
            假設驅動表的行數是 N,執行過程就要掃描驅動表 N 行,然後對於每一行,到被驅動表上匹配一次。
            因此整個執行過程,近似複雜度是 N + N*2*log M。
            顯然,N對掃描行數的影響更大,因此應該讓小表來做驅動表。
                如果你沒覺得這個影響有那麼“顯然”, 可以這麼理解:
                N 擴大 1000 倍的話,掃描行數就會擴大 1000 倍;而 M 擴大 1000 倍,掃描行數擴大不到10 倍。
            通過上面的分析得到了兩個結論:
                1. 使用 join 語句,效能比強行拆成多個單表執行 SQL 語句的效能要好;
                2. 如果使用 join 語句的話,需要讓小表做驅動表。
            但是需要注意,這個結論的前提是“可以使用被驅動表的索引”

圖28

  • in和exsits優化

    原則:小表驅動大表,即小的資料集驅動大的資料集

     in:
         當B表的資料集小於A表的資料集時,in優於exists
         select * from A where id in (select id from B)
         可以理解為:
         for(select id from B){
             select * from A where A.id = id
         }
     
     exists:
         當A表的資料集小於B表的資料集時,exists優於in將主查詢A的資料,
         放到子查詢B中做條件驗證,根據驗證結果(true或false)來決定主查詢的資料是否保留
         
         select * from A where exists (select 1 from B where B.id = A.id)
         可以理解為:
         for(select * from A){
             select * from B where B.id = A.id
         }
         A表與B表的ID欄位應建立索引
     
     1、EXISTS(subquery)只返回TRUE或FALSE,因此子查詢中的SELECT * 也可以用SELECT 1替換,官方說法是實際執行時會忽略SELECT清單,因此沒有區別
     2、EXISTS子查詢的實際執行過程可能經過了優化而不是我們理解上的逐條對比
     3、EXISTS子查詢往往也可以用JOIN來代替,何種最優需要具體問題具體分析
    
  • count(*)查詢優化

     臨時關閉mysql查詢快取,為了檢視sql多次執行的真實時間
     set global query_cache_size=0;
     set global query_cache_type=0;
     
     
     explain select count(1) from employees;
     explain select count(id) from employees;
     explain select count(name) from employees;
     explain select count(*) from employees;
    

圖29

    四個sql的執行計劃一樣,說明這四個sql執行效率應該差不多,區別在於根據某個欄位count不會統計欄位為null值的資料行
    為什麼mysql最終選擇輔助索引而不是主鍵聚集索引?
        因為二級索引相對主鍵索引儲存資料更少,檢索效能應該更高
        
    優化方法
        1、查詢mysql自己維護的總行數
            對於myisam儲存引擎的表做不帶where條件的count查詢效能是很高的,
            因為myisam儲存引擎的表的總行數會被mysql儲存在磁碟上,查詢不需要計算
            可自行建表測試
            對於innodb儲存引擎的表mysql不會儲存表的總記錄行數,查詢count需要實時計算
        
        
        2、show table status
            如果只需要知道表總行數的估計值可以用如下sql查詢,效能很高
            
        3、將總數維護到Redis裡
            插入或刪除表資料行的時候同時維護redis裡的表總行數key的計數值(用incr或decr命令),
            但是這種方式可能不準,很難保證表操作和redis操作的事務一致性
            
        4、增加計數表
            插入或刪除表資料行的時候同時維護計數表,讓他們在同一個事務裡操作
            
    額外分析

    select count(*) from t1
    
    首先要明確的是,在不同的 MySQL 引擎中,count(*) 有不同的實現方式。
        MyISAM引擎把一個表的總行數存在了磁碟上,因此執行 count(*) 的時候會直接返回這個數,效率很高;
        而InnoDB引擎就麻煩了,它執行 count(*) 的時候,需要把資料一行一行地從引擎裡面讀出來,然後累積計數。
        
    這裡需要注意的是,在這裡討論的是沒有過濾條件的 count(*),如果加了where 條件的話,MyISAM 表也是不能返回得這麼快的。
    為什麼InnoDB 不跟MyISAM 一樣,也把數字存起來呢?
    這是因為即使是在同一個時刻的多個查詢,由於多版本併發控制(MVCC)的原因,存在事務(不同會話下可能看見的資料不一樣等情況),
    InnoDB表 “應該返回多少行” 也是不確定的。
    
    這裡說的是沒有過濾條件的 count(*),如果加了where 條件的話,MyISAM 表也是不能返回得這麼快的
    
    
    至於分析效能差別的時候,你可以記住這麼幾個原則:
        1. server 層要什麼就給什麼;
        2. InnoDB 只給必要的值;
        3. 現在的優化器只優化了 count(*) 的語義為“取行數”,其他“顯而易見”的優化並沒有做。
    
    對於 count(主鍵 id) 來說,InnoDB 引擎會遍歷整張表,把每一行的 id值都取出來,返回給 server 層。server 層拿到 id 後,
    判斷是不可能為空的,就按行累加。
    
    對於 count(1) 來說,InnoDB 引擎遍歷整張表,但不取值。server 層對於返回的每一行,放一個數字“1”進去,判斷是不可能為空的,按行累加。
    
    單看這兩個用法的差別的話,你能對比出來,count(1) 執行得要比 count(主鍵 id) 快。因從引擎返回id會涉及到解析資料行,以及拷貝欄位值的操作。
    
    對於 count(欄位) 來說:
        1. 如果這個“欄位”是定義為 not null 的話,一行行地從記錄裡面讀出這個欄位,判斷不能為 null,按行累加;
        2. 如果這個“欄位”定義允許為 null,那麼執行的時候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。
        
    也就是前面的第一條原則,server層要什麼欄位,InnoDB就返回什麼欄位。
    但是 count(*)是例外,並不會把全部欄位取出來,而是專門做了優化,不取值。count(*)肯定不是 null,按行累加。
    
    看到這裡,你一定會說,優化器就不能自己判斷一下嗎,主鍵id肯定非空啊,為什麼不能按照 count(*)來處理,多麼簡單的優化啊。
    當然,MySQL專門針對這個語句進行優化,也不是不可以。
    但是這種需要專門優化的情況太多了,而且 MySQL已經優化過count(*) 了,直接使用這種用法就可以了。
    
    所以結論是:按照效率排序的話,count(欄位)<count(主鍵 id)<count(1)≈count(*)

join語句優化深入剖析

    清空t2的資料重新插入100條
        create procedure insert_emp_t2()
        begin
        declare i int;
        set i =1;
        while(i<=1000)do
        insert into t2(a,b) values(1001-i,i+1);
            set i=i+1;
        end while;
        end;
        call insert_emp_t2();
    
    t1 10000個數據,t2 1000個數據
   
    優化目的:
        Multi- 優化 (MRR)。這個優化的主要目的是儘量使用順序讀盤
     在這之前需要明白,回表肯定是一行行搜尋主鍵索引的,不能批量會表
  • MRR

      以下sql
         select * from t1 where a>=1 and a<=100
         主鍵索引是一棵 B+ 樹,在這棵樹上,每次只能根據一個主鍵 id 查到一行資料。
         因此,回表肯定是一行行搜尋主鍵索引的,基本流程如圖 30所示
    

    圖30

     如果隨著a的值遞增順序查詢的話,id 的值就變成隨機的,那麼就會出現隨機訪問,效能相對較差。
     雖然“按行查”這個機制不能改,但是調整查詢的順序,還是能夠加速的。
     因為大多數的資料都是按照主鍵遞增順序插入得到的,所以我們可以認為,
     如果按照主鍵的遞增順序查詢的話,對磁碟的讀比較接近順序讀,能夠提升讀效能
     
     這,就是 MRR 優化的設計思路。此時,語句的執行流程變成了這樣
         1. 根據索引 ,定位到滿足條件的記錄,將id值放入 read_rnd_buffer 中 ;
         2.  read_rnd_buffer 中的id進行遞增排序;
         3. 排序後的id陣列,依次到主鍵 id 索引中查記錄,並作為結果返回。
         
     這裡,read_rnd_buffer的大小是由 read_rnd_buffer_size 引數控制的。
     如果步驟 1 中,read_rnd_buffer放滿了,就會先執行完步驟 2 和 3,然後清空 read_rnd_buffer。
     之後繼續找索引 a 的下個記錄,並繼續迴圈。
     另外需要說明的是,如果想要穩定地使用MRR 優化的話,需要設定
         set optimizer_switch="mrr_cost_based=off"
         
     (官方文件的說法,是現在的優化器策略,判斷消耗的時候,會更傾向於不使用 MRR,把 mrr_cost_based 設定為 off,就是固定使用 MRR 了。)
     
     使用了MRR 優化後的執行流程和 explain 結果:
    

    MRR 執行流程
    圖31

    從圖的explain 結果中,可以看到 Extra 欄位多了 Using MRR,表示的是用上了MRR 優化。
    而且,由於在 read_rnd_buffer 中按照 id 做了排序,所以最後得到的結果集也是按照主鍵id遞增順序的,也就是與圖30 結果集中行的順序相反
    
    結論:
         MRR能夠提升效能的核心在於,這條查詢語句在索引 a 上做的是一個範圍查詢(也就是說,這是一個多值查詢),
         可以得到足夠多的主鍵 id。這樣通過排序以後,再去主鍵索引查資料,才能體現出“順序性”的優勢
    
  • Batched Key Access

     理解了MRR效能提升的原理,就能理解 MySQL 在 5.6 版本後開始引入的 BatchedKey Access(BKA) 演算法了。這個 BKA 演算法,其實就是對 NLJ演算法的優化
     基於Index Nested-Loop Join 流程圖,看前面的圖
     
     NLJ演算法執行的邏輯是:
         從驅動表 t2,一行行地取出 a 的值,再到被驅動表 t1 去做join。
         也就是說,對於表 t1 來說,每次都是匹配一個值。這時,MRR的優勢就用不上了。
     
     那怎麼才能一次性地多傳些值給表 t1 呢?
         方法就是,從表 t2裡一次性地多拿些行出來,一起傳給表t1。
         既然如此,就把表t2的資料取出來一部分,先放到一個臨時記憶體。這個臨時記憶體不是別人,就是 join_buffer
         通過前面的內容可以知道 join_buffer 在BNL演算法裡的作用,是暫存驅動表的資料。
         但是在 NLJ 演算法裡並沒有用。那麼,剛好就可以複用 join_buffer到 BKA 演算法中。
     
     如下圖Batched Key Access 流程所示,是上面的NLJ演算法優化後的BKA演算法的流程。
    

Batched Key Access 流程

    圖中,在join_buffer中放入的資料是 P1~P100,表示的是隻會取查詢需要的欄位。當然,如果 join buffer 放不下 P1~P100 的所有資料,
    就會把這 100 行資料分成多段執行上圖的流程
    
    那麼,這個 BKA 演算法到底要怎麼啟用呢?
    如果要使用BKA 優化演算法的話,需要在執行SQL語句之前,先設定
        set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
    其中,前兩個引數的作用是要啟用 MRR。這麼做的原因是,BKA 演算法的優化要依賴於MRR

BNL 演算法的效能問題

    在此之前有一個問題需要分析
        使用 Block Nested-Loop Join(BNL) 演算法時,可能會對被驅動表做多次掃描。
        如果這個被驅動表是一個大的冷資料表,除了會導致IO 壓力大以外,還會對系統有什麼影響呢?
        
        由於 InnoDB 對 BuffferPool 的 LRU 演算法做了優化,即:
            第一次從磁碟讀入記憶體的資料頁,會先放在 old 區域。
            如果 1 秒之後這個資料頁不再被訪問了,就不會被移動到 LRU 連結串列頭部,這樣對 BufferPool 的命中率影響就不大。
            
            但是,如果一個使用BNL演算法的join語句,多次掃描一個冷表,而且這個語句執行時間超過 1 秒,就會在再次掃描冷表的時候,把冷表的資料頁移到 LRU 連結串列頭部
            這種情況對應的,是冷表的資料量小於整個Buffer Pool的 3/8,能夠完全放入 old 區域的情況。
            如果這個冷表很大,就會出現另外一種情況:
                業務正常訪問的資料頁,沒有機會進入young 區域。
                
            由於優化機制的存在,一個正常訪問的資料頁,要進入 young 區域,需要隔1秒後再次被訪問到。
            但是,由於join 語句在迴圈讀磁碟和淘汰記憶體頁,進入 old 區域的資料頁,很可能在1秒之內就被淘汰了。
            這樣,就會導致這個 MySQL 例項的 Buffer Pool 在這段時間內,young區域的資料頁沒有被合理地淘汰。
            也就是說,這兩種情況都會影響 Buffer Pool 的正常運作
            
            大表 join操作雖然對IO有影響,但是在語句執行結束後,對 IO 的影響也就結束了。
            但是,對 Buffer Pool 的影響就是持續性的,需要依靠後續的查詢請求慢慢恢復記憶體命中率
            為了減少這種影響,可以考慮增大 join_buffer_size 的值,減少對被驅動表的掃描次數
            
        也就是說,BNL 演算法對系統的影響主要包括三個方面
            1. 可能會多次掃描被驅動表,佔用磁碟 IO 資源;
            2. 判斷join條件需要執行 M*N 次對比(M、N 分別是兩張表的行數),如果是大表就會佔用非常多的 CPU 資源;
            3. 可能會導致Buffer Pool的熱資料被淘汰,影響記憶體命中率。
        
        因此在執行語句之前,需要通過理論分析和檢視 explain 結果的方式,確認是否要使用 BNL演算法。
        如果確認優化器會使用 BNL演算法,就需要做優化。
        優化的常見做法是,給被驅動表的 join 欄位加上索引,把 BNL演算法轉成BKA演算法

BNL轉BKA優化方式

      一些情況下,可以直接在被驅動表上建索引,這時就可以直接轉成 BKA 演算法了。
      但是,有時候確實會碰到一些不適合在被驅動表上建索引的情況。比如下面這個語句:
        explain select * from t2 join t1 on (t2.b=t1.b) where t1.b>=1 and t1.b<=2000;
      
      在資料準備的時候,在表 t1中插入了 1萬行資料,但是經過 where 條件過濾後,需要參與 join 的只有 2000 行資料。
      如果這條語句同時是一個低頻的 SQL 語句,那麼再為這個語句在表 t2 的欄位 b上建立一個索引就很浪費了
      
      但是,如果使用 BNL 演算法來 join 的話,這個語句的執行流程是這樣的:
        1. 把表 t2的所有欄位取出來,存入 join_buffer 中。這個表只有 1000 行,join_buffer_size 預設值是 256k,可以完全存入。
        2. 掃描表 t1,取出每一行資料跟 join_buffer 中的資料進行對比,
            如果不滿足 t2.b=t1.b,則跳過;
            如果滿足 t2.b=t1.b, 再判斷其他條件,也就是是否滿足 t1.b 處於 [1,2000] 的條件,
            如果是,就作為結果集的一部分返回,否則跳過。
      
      對於表t1的每一行,判斷 join 是否滿足的時候,都需要遍歷join_buffer中的所有行。
      因此判斷等值條件的次數是 1000*1 萬=1千萬次,這個判斷的工作量很大。

       這裡執行用了0.6s
       在表t1的欄位b上建立索引會浪費資源,但是不建立索引的話這個語句的等值條件要判斷1 千萬次,想想也是浪費。那麼,有沒有兩全其美的辦法呢?
       這時候,我們可以考慮使用臨時表。使用臨時表的大致思路是:
            1. 把表 t1中滿足條件的資料放在臨時表tmp_t中;
            2. 為了讓join使用 BKA 演算法,給臨時表 tmp_t 的欄位 b加上索引;
            3. 讓表t2和tmp_t做join 操作。
            
       此時,對應的 SQL 語句的寫法如下:
            create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
            insert into temp_t select * from t1 where b>=1 and b<=2000;
            select * from t2 join temp_t on (t2.b=temp_t.b);

圖32

        資料多一點能明顯比較出來 效能得到了大幅提升(我這裡檢視用了0.3S)
        
        分析一下這個過程的消耗
            1. 執行insert語句構造 temp_t表並插入資料的過程中,對錶 t1 做了全表掃描,這裡掃描行數是 1萬。
            2. 之後的 join 語句,掃描表 t2,這裡的掃描行數是 1000;join 比較過程中,做了 1000次帶索引的查詢。
               相比於優化前的 join 語句需要做 1千萬次條件判斷來說,這個優化效果還是很明顯的。
        
        總體來看,不論是在原表上加索引,還是用有索引的臨時表,思路都是讓 join 語句能夠用上被驅動表上的索引,來觸發 BKA 演算法,提升查詢效能
  • 擴充套件 -hash join

     看到這裡你可能發現了,其實上面計算 1千萬次那個操作,看上去有點兒傻。
     如果join_buffer裡面維護的不是一個無序陣列,而是一個雜湊表的話,那麼就不是 1千萬次判 斷,而是 1萬次 hash 查詢。
     這樣的話,整條語句的執行速度就快多了吧?
     對此MySQL 的優化器和執行器並不支援,但是這可以在業務程式碼中實現,實現流程大致如下:
         1. select * from t2;
             取得表 t2 的全部 1000行資料,在業務端存入一個 hash 結構,比如 C++ 裡的 set、PHP 的陣列這樣的資料結構。
         2. select * from t1 where b>=1 and b<=2000; 獲取表 t1 中滿足條件的 2000 行資料。
         3. 把這 2000 行資料,一行一行地取到業務端,到 hash 結構的資料表中尋找匹配的資料。滿足匹配的條件的這行資料,就作為結果集的一行。
         
     理論上,這個過程會比臨時表方案的執行速度還要快一些。這裡我就不驗證了
    

    最後在講join 語句的這兩篇文章中,都只涉及到了兩個表的 join。那麼,現在有一個三個表join的需求

         CREATE TABLE `t1` (
         `id` int(11) NOT NULL,
         `a` int(11) DEFAULT NULL,
         `b` int(11) DEFAULT NULL,
         `c` int(11) DEFAULT NULL,
         PRIMARY KEY (`id`)
         ) ENGINE=InnoDB;
         create table t2 like t1;
         create table t3 like t2;
         
         語句的需求實現如下的 join 邏輯:
             select * from t1 join t2 on (t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c>=X and t2.c>=Y and t3.c>=Z;
             
         現在為了得到最快的執行速度,如果讓你來設計表 t1、t2、t3 上的索引,來支援這個 join語句,你會加哪些索引呢?
         如果希望你用 straight_join 來重寫這個語句,配合你建立的索引,你就需要安排連線順序,主要考慮的因素是什麼呢?
         
             第一原則是要儘量使用 BKA演算法。
             需要注意的是,使用BKA演算法的時候,並不是“先計算兩個表 join的結果,再跟第三個表 join”,而是直接巢狀查詢的。
             
             具體實現是:
                 在 t1.c>=X、t2.c>=Y、t3.c>=Z 這三個條件裡,選擇一個經過過濾以後,資料最少的那個表,作為第一個驅動表。
                 此時,可能會出現如下兩種情況。
                 
                 第一種情況,如果選出來是表t1或者 t3,那剩下的部分就固定了。
                     1. 如果驅動表是 t1,則連線順序是 t1->t2->t3,要在被驅動表字段建立上索引,也就是t2.a 和 t3.b 上建立索引;
                     2. 如果驅動表是 t3,則連線順序是 t3->t2->t1,需要在 t2.b 和 t1.a 上建立索引。
                        同時,還需要在第一個驅動表的欄位c上建立索引。
                     
                 第二種情況是,如果選出來的第一個驅動表是表 t2 的話,則需要評估另外兩個條件的過濾效果。
                 
             總之,整體的思路就是,儘量讓每一次參與 join 的驅動表的資料集,越小越好,因為這樣驅動表就會越小