在上一篇文章中,我和你介紹了 join 語句的兩種算法,分別是 Index Nested-LoopJoin(NLJ) 和 Block Nested-Loop Join(BNL)。算法
咱們發如今使用 NLJ 算法的時候,其實效果仍是不錯的,比經過應用層拆分紅多個語句而後再拼接查詢結果更方便,並且性能也不會差。數組
可是,BNL 算法在大表 join 的時候性能就差多了,比較次數等於兩個表參與 join 的行數的乘積,很消耗 CPU 資源。bash
固然了,這兩個算法都還有繼續優化的空間,咱們今天就來聊聊這個話題。數據結構
爲了便於分析,我仍是建立兩個表 t一、t2 來和你展開今天的問題。oop
爲了便於後面量化說明,我在表 t1 裏,插入了 1000 行數據,每一行的 a=1001-id 的值。也就是說,表 t1 中字段 a 是逆序的。同時,我在表 t2 中插入了 100 萬行數據。性能
create table t1(id int primary key, a int, b int, index(a)); create table t2 like t1; drop procedure idata; delimiter ;; create procedure idata() begin declare i int; set i=1; while(i<=1000)do insert into t1 values(i, 1001-i, i); set i=i+1; end while; set i=1; while(i<=1000000)do insert into t2 values(i, i, i); set i=i+1; end while; end;; delimiter ; call idata();
在介紹 join 語句的優化方案以前,我須要先和你介紹一個知識點,即:Multi-RangeRead 優化 (MRR)。這個優化的主要目的是儘可能使用順序讀盤。測試
在第 4 篇文章中,我和你介紹 InnoDB 的索引結構時,提到了「回表」的概念。咱們先來回顧一下這個概念。回表是指,InnoDB 在普通索引 a 上查到主鍵 id 的值後,再根據一個
個主鍵 id 的值到主鍵索引上去查整行數據的過程。優化
而後,有同窗在留言區問到,回表過程是一行行地查數據,仍是批量地查數據?咱們先來看看這個問題。假設,我執行這個語句:spa
select * from t1 where a>=1 and a<=100;
主鍵索引是一棵 B+ 樹,在這棵樹上,每次只能根據一個主鍵 id 查到一行數據。所以,回表確定是一行行搜索主鍵索引的,設計
基本流程如圖 1 所示。
圖 1 基本回表流程
若是隨着 a 的值遞增順序查詢的話,id 的值就變成隨機的,那麼就會出現隨機訪問,性能相對較差。雖然「按行查」這個機制不能改,可是調整查詢的順序,仍是可以加速的。
由於大多數的數據都是按照主鍵遞增順序插入獲得的,因此咱們能夠認爲,若是按照主鍵的遞增順序查詢的話,對磁盤的讀比較接近順序讀,可以提高讀性能。
這,就是 MRR 優化的設計思路。此時,語句的執行流程變成了這樣:
1. 根據索引 a,定位到知足條件的記錄,將 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 優化的話,須要設置setoptimizer_switch="mrr_cost_based=off"。(官方文檔的說法,是如今的優化器策略,判斷消耗的時候,
會更傾向於不使用 MRR,把 mrr_cost_based 設置爲 off,就是固定使用 MRR 了。)
下面兩幅圖就是使用了 MRR 優化後的執行流程和 explain 結果。
圖 2 MRR 執行流程
圖 3 MRR 執行流程的 explain 結果
從圖 3 的 explain 結果中,咱們能夠看到 Extra 字段多了 Using MRR,表示的是用上了MRR 優化。並且,因爲咱們在 read_rnd_buffer 中按照 id 作了排序,因此最後獲得的結
果集也是按照主鍵 id 遞增順序的,也就是與圖 1 結果集中行的順序相反。
到這裏,咱們小結一下。
MRR 可以提高性能的核心在於,這條查詢語句在索引 a 上作的是一個範圍查詢(也就是說,這是一個多值查詢),能夠獲得足夠多的主鍵 id。這樣經過排序之後,再去主鍵索引
查數據,才能體現出「順序性」的優點。
理解了 MRR 性能提高的原理,咱們就能理解 MySQL 在 5.6 版本後開始引入的 BatchedKey Acess(BKA) 算法了。這個 BKA 算法,其實就是對 NLJ 算法的優化。
咱們再來看看上一篇文章中用到的 NLJ 算法的流程圖:
圖 4 Index Nested-Loop Join 流程圖
NLJ 算法執行的邏輯是:從驅動表 t1,一行行地取出 a 的值,再到被驅動表 t2 去作join。也就是說,對於表 t2 來講,每次都是匹配一個值。這時,MRR 的優點就用不上了。
那怎麼才能一次性地多傳些值給表 t2 呢?方法就是,從表 t1 裏一次性地多拿些行出來,一塊兒傳給表 t2。
既然如此,咱們就把表 t1 的數據取出來一部分,先放到一個臨時內存。這個臨時內存不是別人,就是 join_buffer。
經過上一篇文章,咱們知道 join_buffer 在 BNL 算法裏的做用,是暫存驅動表的數據。可是在 NLJ 算法裏並無用。那麼,咱們恰好就能夠複用 join_buffer 到 BKA 算法中。
如圖 5 所示,是上面的 NLJ 算法優化後的 BKA 算法的流程。
圖 5 Batched Key Acess 流程
圖中,我在 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';
說完了 NLJ 算法的優化,咱們再來看 BNL 算法的優化。
我在上一篇文章末尾,給你留下的思考題是,使用 Block Nested-Loop Join(BNL) 算法時,可能會對被驅動表作屢次掃描。若是這個被驅動表是一個大的冷數據表,除了會致使
IO 壓力大之外,還會對系統有什麼影響呢?
在第 33 篇文章中,咱們說到 InnoDB 的 LRU 算法的時候提到,因爲 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 的影響也就結束了。可是,對爲了減小這種影響,你能夠考慮增大 join_buffer_size 的值,減小對被驅動表的掃描次數。
也就是說,BNL 算法對系統的影響主要包括三個方面:
1. 可能會屢次掃描被驅動表,佔用磁盤 IO 資源;
2. 判斷 join 條件須要執行 M*N 次對比(M、N 分別是兩張表的行數),若是是大表就會佔用很是多的 CPU 資源;
3. 可能會致使 Buffer Pool 的熱數據被淘汰,影響內存命中率。
咱們執行語句以前,須要經過理論分析和查看 explain 結果的方式,確認是否要使用 BNL算法。若是確認優化器會使用 BNL 算法,就須要作優化。優化的常見作法是,給被驅動表
的 join 字段加上索引,把 BNL 算法轉成 BKA 算法。
接下來,咱們就具體看看,這個優化怎麼作?
一些狀況下,咱們能夠直接在被驅動表上建索引,這時就能夠直接轉成 BKA 算法了。
可是,有時候你確實會碰到一些不適合在被驅動表上建索引的狀況。好比下面這個語句:
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
咱們在文章開始的時候,在表 t2 中插入了 100 萬行數據,可是通過 where 條件過濾後,須要參與 join 的只有 2000 行數據。若是這條語句同時是一個低頻的 SQL 語句,那麼再
爲這個語句在表 t2 的字段 b 上建立一個索引就很浪費了。
可是,若是使用 BNL 算法來 join 的話,這個語句的執行流程是這樣的:
1. 把表 t1 的全部字段取出來,存入 join_buffer 中。這個表只有 1000 行,join_buffer_size 默認值是 256k,能夠徹底存入。
2. 掃描表 t2,取出每一行數據跟 join_buffer 中的數據進行對比,
我在上一篇文章中說過,對於表 t2 的每一行,判斷 join 是否知足的時候,都須要遍歷join_buffer 中的全部行。所以判斷等值條件的次數是 1000*100 萬 =10 億次,這個判斷
的工做量很大。
圖 6 explain 結果
圖 7 語句執行時間
能夠看到,explain 結果裏 Extra 字段顯示使用了 BNL 算法。在個人測試環境裏,這條語句須要執行 1 分 11 秒。
在表 t2 的字段 b 上建立索引會浪費資源,可是不建立索引的話這個語句的等值條件要判斷 10 億次,想一想也是浪費。那麼,有沒有一箭雙鵰的辦法呢?
這時候,咱們能夠考慮使用臨時表。使用臨時表的大體思路是:
1. 把表 t2 中知足條件的數據放在臨時表 tmp_t 中;
2. 爲了讓 join 使用 BKA 算法,給臨時表 tmp_t 的字段 b 加上索引;
3. 讓表 t1 和 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 t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);
圖 8 就是這個語句序列的執行效果。
圖 8 使用臨時表的執行效果
能夠看到,整個過程 3 個語句執行時間的總和還不到 1 秒,相比於前面的 1 分 11 秒,性能獲得了大幅提高。接下來,咱們一塊兒看一下這個過程的消耗:
整體來看,不管是在原表上加索引,仍是用有索引的臨時表,咱們的思路都是讓 join 語句可以用上被驅動表上的索引,來觸發 BKA 算法,提高查詢性能。
看到這裏你可能發現了,其實上面計算 10 億次那個操做,看上去有點兒傻。若是join_buffer 裏面維護的不是一個無序數組,而是一個哈希表的話,那麼就不是 10 億次判
斷,而是 100 萬次 hash 查找。這樣的話,整條語句的執行速度就快多了吧?
確實如此。
這,也正是 MySQL 的優化器和執行器一直被詬病的一個緣由:不支持哈希 join。而且,MySQL 官方的 roadmap,也是遲遲沒有把這個優化排上議程。
實際上,這個優化思路,咱們能夠本身實如今業務端。實現流程大體以下:
1. select * from t1;取得表 t1 的所有 1000 行數據,在業務端存入一個 hash 結構,好比 C++ 裏的 set、PHP 的數組這樣的數據結構。
2. select * from t2 where b>=1 and b<=2000; 獲取表 t2 中知足條件的 2000 行數據。
3. 把這 2000 行數據,一行一行地取到業務端,到 hash 結構的數據表中尋找匹配的數據。知足匹配的條件的這行數據,就做爲結果集的一行。
理論上,這個過程會比臨時表方案的執行速度還要快一些。若是你感興趣的話,能夠本身驗證一下。
今天,我和你分享了 Index Nested-Loop Join(NLJ)和 Block Nested-LoopJoin(BNL)的優化方法
在這些優化方法中:
1. BKA 優化是 MySQL 已經內置支持的,建議你默認使用;
2. BNL 算法效率低,建議你都儘可能轉成 BKA 算法。優化的方向就是給被驅動表的關聯字段加上索引;
3. 基於臨時表的改進方案,對於可以提早過濾出小數據的 join 語句來講,效果仍是很好的;
4. MySQL 目前的版本還不支持 hash join,但你能夠配合應用端本身模擬出來,理論上效果要好於臨時表的方案。
最後,我給你留下一道思考題吧。
咱們在講 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; insert into ... //初始化三張表的數據
語句的需求實現以下的 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;
如今爲了獲得最快的執行速度,若是讓你來設計表 t一、t二、t3 上的索引,來支持這個 join語句,你會加哪些索引呢?
同時,若是我但願你用 straight_join 來重寫這個語句,配合你建立的索引,你就須要安排鏈接順序,你主要考慮的因素是什麼呢?
你能夠把你的方案和分析寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上篇文章最後留給你的問題,已經在本篇文章中解答了。
這裏我再根據評論區留言的狀況,簡單總結下。根據數據量的大小,有這麼兩種狀況:
@長傑 和 @老楊同志 提到了數據量小於 old 區域內存的狀況;
@Zzz 同窗,很認真地看了其餘同窗的評論,而且提了一個很深的問題。對被驅動表數據量大於 Buffer Pool 的場景,作了很細緻的推演和分析。
給這些同窗點贊,很是好的思考和討論