Mysql Join語句執行流程

JOIN主要使用 Index Nested-Loop Join 和 Block Nested-Loop Join 算法實現

Index Nested-Loop Join

如果 join on 相關的字段存在索引就使用 Index Nested-Loop Join 算法來進行關聯

如下sql語句的執行過程:

select * from t1 join t2 on (t1.a=t2.a);
  1. 對驅動表 t1 做了全表掃描,這個過程需要掃描 100 行;

  2. 而對於每一行 R,根據 a 字段去表 t2 查找,走的是樹搜索過程。由於我們構造的數據 都是一一對應的,因此每次的搜索過程都只掃描一行,也是總共掃描 100 行;

  3. 所以,整個執行流程,總掃描行數是 200。

假設不使用 join,那我們就只能用單表查詢。我們看看上面這條語句的需求,用單表查詢 怎麼實現。

  1. 執行select * from t1,查出表 t1 的所有數據,這裏有 100 行;

  2. 循環遍歷這 100 行數據: 從每一行 R 取出字段 a 的值 $R.a; 執行select * from t2 where a=$R.a; 把返回的結果和 R 構成結果集的一行。

可以看到,在這個查詢過程,也是掃描了 200 行,但是總共執行了 101 條語句,比直接 join 多了 100 次交互。除此之外,客戶端還要自己拼接 SQL 語句和結果

顯然,這麼做還不如直接 join 好。

怎麼選擇驅動表?

假設驅動表的行數是 N,執行過程就要掃描驅動表 N 行,然後對於每一行,到被驅動表上匹配一次

因此整個執行過程,近似複雜度是 N + N2log2M。顯然,N 對掃描行數的影響更大,因此應該讓小表來做驅動表

結論:

  1. 使用 join 語句,性能比強行拆成多個單表執行 SQL 語句的性能要好;

  2. 如果使用 join 語句的話,需要讓小表做驅動表。

但是,你需要注意,這個結論的前提是“可以使用被驅動表的索引”。

Block Nested-Loop Join

如果關聯語句中沒有索引的話 可能需要掃描 N*M 行 當然Mysql對此有一個優化算法

算法的流程是這樣的:

  1. 把表 t1 的數據讀入線程內存 join_buffer 中,由於我們這個語句中寫的是 select *,因此是把整個表 t1 放入了內存;

  2. 掃描表 t2,把表 t2 中的每一行取出來,跟 join_buffer 中的數據做對比,滿足 join 條 件的,作爲結果集的一部分返回。鄭州市不孕不育醫院:http://www.03913882333.com/

Block Nested-Loop Join 算法的這 N*M 次判斷是內存操作,速度上會快很多,性能也更好

接下來,我們來看一下,在這種情況下,應該選擇哪個表做驅動表。

  1. 兩個表都做一次全表掃描,所以總的掃描行數是 M+N;

  2. 內存中的判斷次數是 M*N。

可以看到,調換這兩個算式中的 M 和 N 沒差別,因此這時候選擇大表還是小表做驅動 表,執行耗時是一樣的。

join_buffer 放不下怎麼辦

如果放不下表 t1 的所有數據話,策略很簡單,就是分段放

執行過程就變成了:

  1. 掃描表 t1,順序讀取數據行放入 join_buffer 中,放完第 88 行 join_buffer 滿了,繼續第 2 步;

  2. 掃描表 t2,把 t2 中的每一行取出來,跟 join_buffer 中的數據做對比,滿足 join 條件 的,作爲結果集的一部分返回;鄭州市不孕不育專科醫院:http://www.zzchbb.com/

  3. 清空 join_buffer;

  4. 繼續掃描表 t1,順序讀取最後的 12 行數據放入 join_buffer 中,繼續執行第 2 步

假設,驅動表的數據行數是 N,需要分 K 段才能完成算法流程,被驅動表的數據行數是 M。 注意,這裏的 K 不是常數,N 越大 K 就會越大,因此把 K 表示爲λ*N,顯然λ的取值範圍 是 (0,1)。 所以,在這個算法的執行過程中:

  1. 掃描行數是 N+λ* N*M;

  2. 內存判斷 N*M 次。

顯然,內存判斷次數是不受選擇哪個表作爲驅動表影響的。而考慮到掃描行數,在 M和 N大小確定的情況下,N小一些,整個算式的結果會更小。

所以結論是,應該讓小表當驅動表。

這裏我需要說明下,什麼叫作“小表”。

在決定哪個表做驅動表的時候,應該是兩個表按照各自的條件過濾, 過濾完成之後,計算參與 join 的各個字段的總數據量,數據量小的那個表,就是“小表”,應該作爲驅動表

join語句怎麼優化

Multi-Range Read 優化

在介紹 join 語句的優化方案之前,我需要先介紹一個知識點,即:Multi-Range Read 優化 (MRR)。這個優化的主要目的是儘量使用順序讀盤

因爲大多數的數據都是按照主鍵遞增順序插入得到的,所以我們可以認爲,如果按照主鍵 的遞增順序查詢的話,對磁盤的讀比較接近順序讀,能夠提升讀性能

這,就是 MRR 優化的設計思路。此時,語句的執行流程變成了這樣:

  1. 根據索引 a,定位到滿足條件的記錄,將 id 值放入 read_rnd_buffer 中 ; 2. 將 read_rnd_buffer 中的 id 進行遞增排序;

  2. 排序後的 id 數組,依次到主鍵 id 索引中查記錄,並作爲結果返回。

另外需要說明的是,如果你想要穩定地使用 MRR 優化的話,需要設置

set optimizer_switch="mrr_cost_based=off"

MRR 能夠提升性能的核心在於,這條查詢語句在索引 a 上做的是一個範圍查詢可以得到足夠多的主鍵 id。這樣通過排序以後,再去主鍵索引查數據,才能體現出“順序性”的優勢

Batched Key Access

這個 Batched Key Access (BKA),其實就是對 NLJ 算法的優化

NLJ 算法執行的邏輯是:從驅動表 t1,一行行地取出 a 的值,再到被驅動表 t2 去做 join。也就是說,對於表 t2 來說,每次都是匹配一個值。這時,MRR 的優勢就用不上了

那怎麼才能一次性地多傳些值給表 t2呢?方法就是,從表 t1 裏一次性地多拿些行出來, 一起傳給表 t2。 既然如此,我們就把表t1 的數據取出來一部分,先放到 join_buffer。 我們知道 join_buffer 在 BNL 算法裏的作用,是暫存驅動表的數據。但是在 NLJ 算法裏並沒有用。那麼,我們剛好就可以複用 join_buffer 到 BKA 算法中。

如果要使用 BKA 優化算法的話,你需要在執行 SQL 語句之前,先設置

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

BNL 算法的性能問題

大表 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 算法

hash join

如果 join_buffer 裏面維護的不是一個無序數組,而是一個哈希表的話,那麼就不是 10 億次判 斷,而是 100 萬次 hash 查找。這樣的話,整條語句的執行速度就快多了吧?

實際上,這個優化思路,我們可以自己實現在業務端。實現流程大致如下:

  1. select * from t1;取得表 t1 的全部 1000 行數據,在業務端存入一個 hash 結構

  2. select * from t2 where b>=1 and b<=2000; 獲取表 t2 中滿足條件的 2000 行 數據。

  3. 把這 2000 行數據,一行一行地取到業務端,到 hash 結構的數據表中尋找匹配的數據。滿足匹配的條件的這行數據,就作爲結果集的一行。


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