到底能不能用 join
網際網路上一直流傳著各大公司的 MySQL 軍規,其中關於 join 的描述,有些公司不推薦使用 join,而有些公司則規定有條件的使用 join, 它們都是教條式的規定,也沒有詳細說其中的原因,這就很容出現只知道這麼用,但是不知道為什麼的情況
那到底能不能使用 join, 什麼情況下適合用join,什麼情況下不適合用 join, join 有哪些使用原則呢?
本文將詳細講述 join 的執行流程、分析 join 的複雜度,並解答上面的幾個常見問題,讓讀者能詳細瞭解 join 的原理,做到知其然,知其所以然
測試資料準備
為了更好的分析 join 的工作原理,需要準備一些測試資料,我們分別建立 ta
tb
兩個表,定義 insert_data
函式,然後往 ta
、 tb
表新增測試資料,具體如下所示:
- 建立測試表
Create Table: CREATE TABLE `ta` ( `id` int(11) NOT NULL, `a` int(11) NOT NULL, `b` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `a` (`a`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 Create Table: CREATE TABLE `tb` ( `id` int(11) NOT NULL, `a` int(11) NOT NULL, `b` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `a` (`a`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 定義插入資料函式
定義一個插入資料的函式,用於往 tb
表中插入資料
delimieter ;;
create procedure insert_data()
begin
declare i int;
set i=1;
while( i <= 1000 ) do
insert into tb values(i,i,i);
set i=i+1;
end while;
end;;
delimiter ;
- 新增測試資料
呼叫 insert_data 函式 往tb
tb
表前100條資料插入到 ta
表中
mysql> call insert_data();
mysql> insert into ta (select * from tb where tb.a <= 100);
簡述
MySQL中join主要用到 Simple nested-loop
、 Block nested-loop
、Index nested-loop
三種演算法,下面針對這幾種演算法的流程逐一進行分析,在這之前,有幾點需要說明下
-
本文所有測試都是在 mysql5.7 版本上進行的
-
由於mysql會對join的連線方式做優化,為了便於分析 join 的執行過程,文中的 sql 語句 統一使用我們指定的連線方式執行查詢,關鍵字是 straight_join
Simple nested-loop 演算法
在 MySQL中執行 select * from ta straight_join tb on ta.a = tb.b;
,語句的 explain 的結果如下:
在這個語句中,ta
是驅動表,tb
是被驅動表,由於表 tb
上 b
欄位沒有索引,所以針對 tb
表需要走全表掃描
語句的執行流程如下:
- 從
ta
表讀取一條資料,記為Da
- 從
Da
中取出欄位a
去tb
表查詢 - 取出
tb
表滿足條件的資料,跟Da
資料組合成一行,作為結果集返回 - 接著讀取
ta
表剩下的資料,重複執行步驟1 到 步驟3,直到讀取完ta
表記錄
上面的流程是迴圈讀取 ta
表記錄,把每一行記錄中的 a
欄位值, 去 tb
表中查詢滿足條件的記錄,這個過程就類似下面程式碼中的巢狀for迴圈
for each row in ta matching range
{
for each row in tb matching
{
if row satisfies join conditions, send to client
}
}
- 效能分析
每讀取一條 ta
表記錄,就需要掃描一次 tb
表的記錄並與記錄中 b
欄位的值進行比較
所以總共掃描了 100 * 1000 = 10 萬 行記錄,進行了 10萬次 比較操作
可以看出,總的掃描行數和總比較次數跟 ta
和 tb
表總記錄數有關,記錄數越大,總掃描行數和比較次數越大
假如 ta
表和 tb
表總記錄數 各增加 10 倍,那麼,總的掃描行數和比較次數會增加 100 倍,這個效率就非常低下了
基於以上原因,MySQL 沒有選擇使用 Simple nested-loop
演算法了,而是使用了 Block nested-loop
演算法
Block nested-loop演算法
Block nested-loop
演算法對 Simple nested-loop
演算法進行了優化,它引入了 join buffer
join buffer
主要用於優化不帶索引條件的 join 查詢,它會快取連線 過程中的用到的欄位,對於減少掃描次數和比較次數很有幫助
join_buffer_size
引數可以控制 join_buffer
的大小,MySQL 預設的
join_buffer_size
是 256K,可以通過 select @@join_buffer_size
查詢目前設定的大小
現以執行 select * from ta straight_join tb on ta.a = tb.b;
語句為例來說說明 Block nested-loop
的執行流程
-
把
ta
表所有記錄讀入 join buffer 中,join buffer 只快取連線過程中必須的資料,這裡使用了select *
,所以,ta
表的記錄的所有欄位都會放入 join buffer 中 -
掃描
tb
表,讀取每一行記錄,跟 join buffer 中的資料對比,滿足條件的資料,將會作為結果集返回
- 效能分析
整個過程中,讀取 ta
表所有記錄,讀取 tb
表所有記錄,相當於 掃描了一遍 ta
表和 tb
表,所以掃描總行數是 100 + 1000 = 1100 行
逐行遍歷 tb
表,與 join buffer 中的資料比較,由於 join buffer 是無序的,所以,對於 tb
表每一條記錄,都需要在 join buffer 中比較 100 次,因此總的比較次數是 1000 * 100 = 10萬次
和 Simple nested-loop
比起來,由於有 join buffer 的幫助,Block nested loop
總掃描行數減少了很多,總共掃描了 100 + 1000 = 1100 行
雖然兩者總比較次數都是 10 萬次,但是,Block nested loop
的比較是在記憶體中的 join buffer 中進行的,所以,速度上應該會快很多,效能相對也好一些
如果驅動表 ta
資料行數是 M ,被驅動表 tb
資料行數是 N ,總的掃描行數為 M + N , 總的比較次數為 M * N , 所以整個執行過程 近似的複雜度是:M + N + M * N
上述複雜度公式中,M 和 N 調換的話,複雜度不變,所以,這時候選擇小表還是大表作為驅動表都一樣
join buffer 大小不夠用的問題
上面 Block nested loop
執行流程中,第一步是把 整個ta
表資料全部讀入 join buffer 中,這裡由於 ta
表資料少,join buffer 可以放得下全部的資料
如果 join buffer 的大小不足以快取 ta
表所有資料,該怎麼辦呢?這時候的執行流程又是怎樣的呢 ?
答案是:join buffer 分段快取 ta
表資料,處理完之後,清空快取,然後再快取 ta
表剩下的資料
現假設 join buffer 只能放得下 60 行 ta
表記錄,執行流程就變成了:
-
遍歷
ta
表,順序讀取 60 條記錄放入 join buffer 中,此時,join buffer 滿了 -
遍歷
tb
表,取出得每一行,跟 join buffer 中得資料比較,組合滿足條件得資料,作為結果集返回 -
清空 join buffer
-
接著上次的位置繼續順序掃描
ta
表,把剩下得 40行資料讀入 join buffer 中,緊接著執行第 2 步 到 第 4 步,直到 讀取完ta
表記錄
- 效能分析
由於 ta
表是分兩次讀入 join buffer 的,所以需要掃描兩次 tb
表,所以總掃描行數是 100 + 1000 * 2 = 2100 行
總的比較次數依然保持不變,是:(60 + 40) * 1000 = 10萬次
從上面的結果可以看出,join buffer 分段快取 的效能要比 一次快取全部驅動表必需資料的方式 要差一些,也就是說,join buffer 分的段數越多,效能相對越差,在驅動表數量行數不變的情況下,分段數的取決於 join_buffer_size
引數的大小,這個引數越大,分段數或越小,反之越大, 所以有些地方建議通過調大 join_buffer_size 引數來提升 join 查詢速度的方法,原因也在於此
如果驅動表 ta
資料行數是 M ,被驅動表 tb
資料行數是 N, join buffer 分段數是 K ,則總掃描行數為 M + K * N , 總的比較次數為 M * N , 所以整個執行過程 近似的複雜度是: M + K * N + M * N
顯然 K 越小,複雜度越小,效能就越好,K 的最小值是 1 ,也就是 驅動表中所必需的欄位能全部快取到 join buffer 中
而 K 是與 驅動表資料行數 M 和 join_buffer_size
引數相關的,後者通常不會經常變化,所以 M 越小, K 就越小,K 越小,複雜度越小,效能就越好,K 的最小值是 1 ,也就是 驅動表中所必需的欄位能全部快取到 join buffer 中
因此,選擇小表作為驅動表,查詢效能更好
Index nested-loop演算法
分析完上面兩種演算法,接著來看下 Index nested-loop
演算法的執行流程
在 MySQL 中執行 select * from ta straight_join tb on ta.a = tb.a;
, 語句的 explain 結果如下:
上述結果中,被驅動表 tb
的欄位 a
上有索引,所以,連線的過程中能用上索引,它的執行流程是:
-
讀取一行
ta
表記錄, 記為Da
-
從
Da
中取出a
欄位去tb
表查詢 -
讀取
tb
表中滿足條件的記錄,和Da
組合,作為結果集返回 -
重複執行 步驟1 到 步驟 3,直到讀取完
ta
表的記錄
- 效能分析
上面的流程是遍歷 ta
表,然後從讀出的每行記錄中取出 a
的值 去 tb
表查詢
由於 tb
表的 a
欄位上有索引,所以查詢 tb
表記錄的時候,走的是 B+ 樹的查詢
我們準備的測試資料中 ta
表 a
欄位和tb
表 a
欄位是一一對應的,因此對於 ta
表的一行記錄,在 tb
表中需要做兩次 B+ 樹查詢,一次是普通索引樹的查詢,一次是回表查詢
總的掃描行數是: 100 + 100 = 200 行( tb
表的兩次B+樹查詢是由掃描一次表導致的,所以這裡把tb
表總掃描次數當作100次)
總的比較次數是: 100 次
如果驅動表 ta
資料行數是 M ,被驅動表 tb
資料行數是 N, 對於驅動表的一行資料,被驅動表需要先查詢 a
索引的 B+ 樹,再查詢主鍵索引 B+ 樹,所以被驅動表查詢一行的複雜度是:2 * log2N
總掃描行數為 M + M * 2 * log2N , 總的比較次數為 M , 所以整個執行過程 近似的複雜度是: M + M * 2 * log2N + M = 2 * M * ( 1 + log2N )
近似複雜度公式中,變數是 M 和 N , 很顯然,M 對結果影響更大,M 表示的是 驅動表 的資料行數,因此,選擇 小表 作為驅動表能顯著降低複雜度,提升查詢速度
結果分析
上面分析了 Simple nested loop
、 Block nested loop
、Index nested loop
這三種演算法的執行流程,並詳細分析了每種演算法的複雜度,根據分析的結果就可以回答本文開頭的幾個問題了
- 能不能使用join
-
如果能用上被驅動表上的索引,即可以用上
Index nested loop
演算法的話,是可以使用 join 的 -
如果使用
Block nested loop
演算法的話,掃描行數和比較次數比較多,會佔用大量的系統資源,特別是對於大表來說,查詢速度和系統性能是無法接受的,所以,這種情況下,能不用join就不用join了
如果 explain 結果的 Extra 欄位包含 ' Using join buffer (Block Nested Loop) ' 字串的話,表示使用了 ' Block nested loop ' 演算法
- join 使用原則
在分析 Index nested loop
的複雜度之後,得出一個結論: 選擇小表作為驅動表,查詢效能更好
對於 Block nested loop
的複雜度分兩種情況
-
join buffer 沒有分段, 此時不論選擇小表還是大表作為驅動表,複雜度都一樣
-
join buffer 分段了,此時選擇小表作為驅動表,複雜度更低,查詢效能更好,相比join buffer 沒有分段,實際的場景中,join buffer 分段的情況應該更多
綜合來講: 使用 join 應該選擇小表 作為驅動表
- 如何選擇 "小" 表
上面 join 使用原則
中講到選擇小表作為驅動表,這裡的 小表 並不是指表資料行數少,而是指參與join的表,按照查詢條件分別得到結果集,結果集小的表作為驅動表
比如:有以下兩個 SQL 語句
語句1:select * from ta straight_join tb on ta.b = tb.b where tb.id <= 20;
語句2:select * from tb straight_join ta on ta.b = tb.b where tb.id <= 20;
ta
表和 tb
表麼一行資料大小是一樣的,語句1 使用 ta
作為驅動表,需要把 ta
表所有行資料(100行)放入 join buffer 中,但是 語句2 使用 tb
作為驅動表,只需要把 tb
表前20行資料放入 join buffer, 也即 tb
表只有20行資料參與join, 這麼一比較,tb
表查詢的結果集更小,所以應該選擇資料行數更多但是查詢的結果集更小的 tb
表作為驅動表
小結
本文詳細討論了 Simple nested loop
、 Block nested loop
、Index nested loop
這三種演算法,根據演算法的執行流程定量的分析了各個演算法的複雜度,再根據複雜度分析了 join 常見的一些問題,更多關於 join 的介紹請參考 MySQL 官網