點擊上方"程序員歷小冰",選擇「置頂或者星標」程序員
你的關注意義重大!
web
你們好,我是歷小冰,今天咱們來學習和吐槽一下 MySQL 的 Join 功能。
算法
關於MySQL 的 join,你們必定了解過不少它的「軼事趣聞」,好比兩表 join 要小表驅動大表,阿里開發者規範禁止三張表以上的 join 操做,MySQL 的 join 功能弱爆了等等。這些規範或者言論亦真亦假,時對時錯,須要你們本身對 join 有深刻的瞭解後才能清楚地理解。數據庫
下面,咱們就來全面的瞭解一下 MySQL 的 join 操做。編程
正文
在平常數據庫查詢時,咱們常常要對多表進行連表操做來一次性得到多個表合併後的數據,這是就要使用到數據庫的 join 語法。join 是在數據領域中十分常見的將兩個數據集進行合併的操做,若是你們瞭解的多的話,會發現 MySQL,Oracle,PostgreSQL 和 Spark 都支持該操做。本篇文章的主角是 MySQL,下文沒有特別說明的話,就是以 MySQL 的 join 爲主語。而 Oracle ,PostgreSQL 和 Spark 則能夠算作將其吊打的大boss,其對 join 的算法優化和實現方式都要優於 MySQL。緩存
MySQL 的 join 有諸多規則,可能稍有不慎,可能一個很差的 join 語句不只會致使對某一張表的全表查詢,還有可能會影響數據庫的緩存,致使大部分熱點數據都被替換出去,拖累整個數據庫性能。微信
因此,業界針對 MySQL 的 join 總結了不少規範或者原則,好比說小表驅動大表和禁止三張表以上的 join 操做。下面咱們會依次介紹 MySQL join 的算法,和 Oracle 和 Spark 的 join 實現對比,並在其中穿插解答爲何會造成上述的規範或者原則。併發
對於 join 操做的實現,大概有 Nested Loop Join (循環嵌套鏈接),Hash Join(散列鏈接) 和 Sort Merge Join(排序歸併鏈接) 三種較爲常見的算法,它們各有優缺點和適用條件,接下來咱們會依次來介紹。分佈式
MySQL 中的 Nested Loop Join 實現
Nested Loop Join 是掃描驅動表,每讀出一條記錄,就根據 join 的關聯字段上的索引去被驅動表中查詢對應數據。它適用於被鏈接的數據子集較小的場景,它也是 MySQL join 的惟一算法實現,關於它的細節咱們接下來會詳細講解。ide
MySQL 中有兩個 Nested Loop Join 算法的變種,分別是 Index Nested-Loop Join 和 Block Nested-Loop Join。
Index Nested-Loop Join 算法
下面,咱們先來初始化一下相關的表結構和數據
CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;
delimiter ;;
# 定義存儲過程來初始化t1
create procedure init_data()
begin
declare i int;
set i=1;
while(i<=10000)do
insert into t1 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
# 調用存儲過來來初始化t1
call init_data();
# 建立並初始化t2
create table t2 like t1;
insert into t2 (select * from t1 where id<=500)
有上述命令可知,這兩個表都有一個主鍵索引 id 和一個索引 a,字段 b 上無索引。存儲過程 init_data 往表 t1 裏插入了 10000 行數據,在表 t2 裏插入的是 500 行數據。
爲了不 MySQL 優化器會自行選擇表做爲驅動表,影響分析 SQL 語句的執行過程,咱們直接使用 straight_join 來讓 MySQL 使用固定的鏈接表順序進行查詢,以下語句中,t1是驅動表,t2是被驅動表。
select * from t2 straight_join t1 on (t2.a=t1.a);
使用咱們以前文章介紹的 explain 命令查看一下該語句的執行計劃。
從上圖能夠看到,t1 表上的 a 字段是由索引的,join 過程當中使用了該索引,所以該 SQL 語句的執行流程以下:
從 t2 表中讀取一行數據 L1;
使用L1 的 a 字段,去 t1 表中做爲條件進行查詢;
取出 t1 中知足條件的行, 跟 L1組成相應的行,成爲結果集的一部分;
重複執行,直到掃描完 t2 表。
這個流程咱們就稱之爲 Index Nested-Loop Join,簡稱 NLJ,它對應的流程圖以下所示。
須要注意的是,在第二步中,根據 a 字段去表t1中查詢時,使用了索引,因此每次掃描只會掃描一行(從explain結果得出,根據不一樣的案例場景而變化)。
假設驅動表的行數是N,被驅動表的行數是 M。由於在這個 join 語句執行過程當中,驅動表是走全表掃描,而被驅動表則使用了索引,而且驅動表中的每一行數據都要去被驅動表中進行索引查詢,因此整個 join 過程的近似複雜度是 N2log2M。顯然,N 對掃描行數的影響更大,所以這種狀況下應該讓小表來作驅動表。
固然,這一切的前提是 join 的關聯字段是 a,而且 t1 表的 a 字段上有索引。
若是沒有索引時,再用上圖的執行流程時,每次到 t1 去匹配的時候,就要作一次全表掃描。這也致使整個過程的時間複雜度編程了 N * M,這是不可接受的。因此,當沒有索引時,MySQL 使用 Block Nested-Loop Join 算法。
Block Nested-Loop Join
Block Nested-Loop Join的算法,簡稱 BNL,它是 MySQL 在被驅動表上無可用索引時使用的 join 算法,其具體流程以下所示:
把表 t2 的數據讀取當前線程的 join_buffer 中,在本篇文章的示例 SQL 沒有在 t2 上作任何條件過濾,因此就是講 t2 整張表 放入內存中;
掃描表 t1,每取出一行數據,就跟 join_buffer 中的數據進行對比,知足 join 條件的,則放入結果集。
好比下面這條 SQL
select * from t2 straight_join t1 on (t2.b=t1.b);
這條語句的 explain 結果以下所示。能夠看出
能夠看出,此次 join 過程對 t1 和 t2 都作了一次全表掃描,而且將表 t2 中的 500 條數據所有放入內存 join_buffer 中,而且對於表 t1 中的每一行數據,都要去 join_buffer 中遍歷一遍,都要作 500 次對比,因此一共要進行 500 * 10000 次內存對比操做,具體流程以下圖所示。
主要注意的是,第一步中,並非將表 t2 中的全部數據都放入 join_buffer,而是根據具體的 SQL 語句,而放入不一樣行的數據和不一樣的字段。好比下面這條 join 語句則只會將表 t2 中符合 b >= 100 的數據的 b 字段存入 join_buffer。
select t2.b,t1.b from t2 straight_join t1 on (t2.b=t1.b) where t2.b >= 100;
join_buffer 並非無限大的,由 join_buffer_size 控制,默認值爲 256K。當要存入的數據過大時,就只有分段存儲了,整個執行過程就變成了:
掃描表 t2,將符合條件的數據行存入 join_buffer,由於其大小有限,存到100行時滿了,則執行第二步;
掃描表 t1,每取出一行數據,就跟 join_buffer 中的數據進行對比,知足 join 條件的,則放入結果集;
清空 join_buffer;
再次執行第一步,直到所有數據被掃描完,因爲 t2 表中有 500行數據,因此一共重複了 5次
這個流程體現了該算法名稱中 Block 的由來,分塊去執行 join 操做。由於表 t2 的數據被分紅了 5 次存入 join_buffer,致使表 t1 要被全表掃描 5次。
所有存入 | 分5次存入 | |
---|---|---|
內存操做 | 10000 * 500 | 10000 * (100 * 5) |
掃描行數 | 10000 + 500 | 10000 * 5 + 500 |
如上所示,和表數據能夠所有存入 join_buffer 相比,內存判斷的次數沒有變化,都是兩張錶行數的乘積,也就是 10000 * 500,可是被驅動表會被屢次掃描,每多存入一次,被驅動表就要掃描一遍,影響了最終的執行效率。
基於上述兩種算法,咱們能夠得出下面的結論,這也是網上大多數對 MySQL join 語句的規範。
被驅動表上有索引,也就是可使用Index Nested-Loop Join 算法時,可使用 join 操做。
不管是Index Nested-Loop Join 算法或者 Block Nested-Loop Join 都要使用小表作驅動表。
由於上述兩個 join 算法的時間複雜度至少也和涉及表的行數成一階關係,而且要花費大量的內存空間,因此阿里開發者規範所說的嚴格禁止三張表以上的 join 操做也是能夠理解的了。
可是上述這兩個算法只是 join 的算法之一,還有更加高效的 join 算法,好比 Hash Join 和 Sorted Merged join。惋惜這兩個算法 MySQL 的主流版本中目前都不提供,而 Oracle ,PostgreSQL 和 Spark 則都支持,這也是網上吐槽 MySQL 弱爆了的緣由(MySQL 8.0 版本支持了 Hash join,可是8.0目前還不是主流版本)。
其實阿里開發者規範也是在從 Oracle 遷移到 MySQL 時,由於 MySQL 的 join 操做性能太差而定下的禁止三張表以上的 join 操做規定的 。
Hash Join 算法
Hash Join 是掃描驅動表,利用 join 的關聯字段在內存中創建散列表,而後掃描被驅動表,每讀出一行數據,並從散列表中找到與之對應數據。它是大數據集鏈接操時的經常使用方式,適用於驅動表的數據量較小,能夠放入內存的場景,它對於沒有索引的大表和並行查詢的場景下可以提供最好的性能。惋惜它只適用於等值鏈接的場景,好比 on a.id = where b.a_id。
仍是上述兩張表 join 的語句,其執行過程以下
將驅動表 t2 中符合條件的數據取出,對其每行的 join 字段值進行 hash 操做,而後存入內存中的散列表中;
遍歷被驅動表 t1,每取出一行符合條件的數據,也對其 join 字段值進行 hash 操做,拿結果到內存的散列表中查找匹配,若是找到,則成爲結果集的一部分。
能夠看出,該算法和 Block Nested-Loop Join 有相似之處,只不過是將無序的 Join Buffer 改成了散列表 hash table,從而讓數據匹配再也不須要將 join buffer 中的數據所有遍歷一遍,而是直接經過 hash,以接近 O(1) 的時間複雜度得到匹配的行,這極大地提升了兩張表的 join 速度。
不過因爲 hash 的特性,該算法只能適用於等值鏈接的場景,其餘的鏈接場景均沒法使用該算法。
Sorted Merge Join 算法
Sort Merge Join 則是先根據 join 的關聯字段將兩張表排序(若是已經排序好了,好比字段上有索引則不須要再排序),而後在對兩張表進行一次歸併操做。若是兩表已經被排過序,在執行排序合併鏈接時不須要再排序了,這時Merge Join的性能會優於Hash Join。Merge Join可適於於非等值Join(>,<,>=,<=,可是不包含!=,也即<>)。
須要注意的是,若是鏈接的字段已經有索引,也就說已經排好序的話,能夠直接進行歸併操做,可是若是鏈接的字段沒有索引的話,則它的執行過程以下圖所示。
遍歷表 t2,將符合條件的數據讀取出來,按照鏈接字段 a 的值進行排序;
遍歷表 t1,將符合條件的數據讀取出來,也按照鏈接字段 a 的值進行排序;
將兩個排序好的數據進行歸併操做,得出結果集。
Sorted Merge Join 算法的主要時間消耗在於對兩個表的排序操做,因此若是兩個表已經按照鏈接字段排序過了,該算法甚至比 Hash Join 算法還要快。在一邊狀況下,該算法是比 Nested Loop Join 算法要快的。
下面,咱們來總結一下上述三種算法的區別和優缺點。
Nested Loop Join | Hash Join | Sorted Merge Join | |
---|---|---|---|
鏈接條件 | 適用於任何條件 | 只適用於等值鏈接(=) | 等值或非等值鏈接(>,<,=,>=,<=),‘<>’除外 |
主要消耗資源 | CPU、磁盤I/O | 內存、臨時空間 | 內存、臨時空間 |
特色 | 當有高選擇性索引或進行限制性搜索時效率比較高,可以快速返回第一次的搜索結果 | 當缺少索引或者索引條件模糊時,Hash Join 比 Nested Loop 有效。一般比 Merge Join 快。在數據倉庫環境下,若是表的紀錄數多,效率高 | 當缺少索引或者索引條件模糊時,Sort Merge Join 比 Nested Loop 有效。當鏈接字段有索引或者提早排好序時,比 hash join 快,而且支持更多的鏈接條件 |
缺點 | 無索引或者表記錄多時效率低 | 創建哈希表須要大量內存,第一次的結果返回較慢 | 全部的表都須要排序。它爲最優化的吞吐量而設計,而且在結果沒有所有找到前不返回數據 |
須要索引 | 是(沒有索引效率太差) | 否 | 否 |
對於 Join 操做的理解
講完了 Join 相關的算法,咱們這裏也聊一聊對於 join 操做的業務理解。
在業務不復雜的狀況下,大多數join並非無可替代。好比訂單記錄裏通常只有訂單用戶的 user_id,返回信息時須要取得用戶姓名,可能的實現方案有以下幾種:
一次數據庫操做,使用 join 操做,訂單表和用戶表進行 join,連同用戶名一塊兒返回;
兩次數據庫操做,分兩次查詢,第一次得到訂單信息和 user_id,第二次根據 user_id 取姓名,使用代碼程序進行信息合併;
使用冗餘用戶名稱或者從 ES 等非關係數據庫中讀取。
上述方案都能解決數據聚合的問題,並且基於程序代碼來處理,比數據庫 join 更容易調試和優化,好比取用戶姓名不從數據庫中取,而是先從緩存中查找。
固然, join 操做也不是一無可取,因此技術都有其使用場景,上邊這些方案或者規則都是互聯網開發團隊總結出來的,適用於高併發、輕寫重讀、分佈式、業務邏輯簡單的狀況,這些場景通常對數據的一致性要求都不高,甚至容許髒讀。
可是,在金融銀行或者財務等企業應用場景,join 操做則是不可或缺的,這些應用通常都是低併發、頻繁複雜數據寫入、CPU密集而非IO密集,主要業務邏輯經過數據庫處理甚至包含大量存儲過程、對一致性與完整性要求很高的系統。
-關注我
本文分享自微信公衆號 - 程序員歷小冰(gh_a1d0b50d8f0a)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。