一 、join應如何優化
先列出答案:
1、為join的連接條件增加索引(減少內層表的循環次數)
2、盡量用小表join大表(其本質就是減少外層循環的數據次數)
3、增大join buffer size的大小(一次緩存的數據越多,那么外層表循環的次數就越少)
4、減少不必要的字段查詢(字段越少,join buffer 所緩存的數據就越多,外層表的循環次數就越少)
5、如果是大表join大表,這種情況對大表建立分區表再進行join,效果會比較明顯。
注意:
1.join優化的最重要的步驟是給join連接字段建立索引,這樣才能大幅度降低查詢速度;
2.小表join大表能提高查詢速度有2個前提:1.當連接字段為索引字段時才可提高查詢速度,如果連接字段為非索引字段則沒有什么效果;2.join連接需是left join或right join才可以,因為inner join的join順序是mysql會根據優化器自行決定查詢順序,比如a表join b表,mysql在執行查詢的時候可能先查b表再查a表(即把b表作為驅動表)。
3.網上有很一些文章說”小表join大表能提高查詢速度是錯誤的,理由是mysql執行器不會根據我們join的順序去查詢,比如a表join b表,mysql在執行查詢的時候可能先查b表再查a表(把b表作為驅動表)”,這個說法其實在inner join的前提下才是有效的。
二、實驗驗證
創建表,並插入數據
說明:user表為大表(100萬條數據),user2為小表(1000條數據),兩個表結構一致,都只含有一個索引,即主鍵(id)索引
-- 創建表user+插入數據(100萬條) create table user(id bigint not null primary key auto_increment, name varchar(20) not null default '' comment '姓名', age tinyint not null default 0 comment 'age', gender char(1) not null default 'M' comment '性別', phone varchar(16) not null default '' comment '手機號', create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間', update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間' ) engine = InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用戶信息表'; CREATE PROCEDURE insert_user_data(num INTEGER) BEGIN DECLARE v_i int unsigned DEFAULT 0; set autocommit= 0; WHILE v_i < num DO insert into user(`name`, age, gender, phone) values (CONCAT('lyn',v_i), mod(v_i,120), 'M', CONCAT('152',ROUND(RAND(1)*100000000))); SET v_i = v_i+1; END WHILE; commit; END call insert_user_data(1000000); -- 創建表user2+插入數據(1000條) create table user2 select * from user where 1=2;-- 復制表,僅復制表結構(不會創建自增主鍵,索引,需手工創建) ALTER TABLE `user2` ADD PRIMARY KEY ( `id` ) ;-- 創建主鍵索引 CREATE PROCEDURE insert_user2_data(num INTEGER) BEGIN DECLARE v_i int unsigned DEFAULT 0; set autocommit= 0; WHILE v_i < num DO insert into user2(`name`, age, gender, phone) values (CONCAT('lyn',v_i), mod(v_i,120), 'M', CONCAT('152',ROUND(RAND(1)*100000000))); SET v_i = v_i+1; END WHILE; commit; END call insert_user2_data(1000);
測試
說明:下面測試按join的連接字段是否為索引列分2種情況測試,先測試大表join小表,再測試小表join大表,分別執行3次,注釋中記錄了3次的查詢時間
-- join的連接字段為索引列 SELECT * from user u LEFT JOIN user2 u2 on u.id = u2.id;-- 3.681s 3.770s 3.650s SELECT * from user2 u2 LEFT JOIN user u on u.id = u2.id;-- 0.002s 0.002s 0.003s -- join的連接字段為非索引列 SELECT * from user u LEFT JOIN user2 u2 on u.name = u2.name;-- 124.450s 139.875s 142.904s SELECT * from user2 u2 LEFT JOIN user u on u.name = u2.name;-- 140.093s 142.917s 139.737s
通過上述測試結果發現:1.join的連接字段為索引列比非索引列快了十條街;2.在join的連接字段為索引列時,小表join大表比大表join小表快了十條街,在join的連接字段為非索引列時,小表join大表與大表join小表的查詢速度似乎差不多。這足以驗證第一節的join優化結論。
分析
下面看下執行計划
-- 連接字段為索引列 EXPLAIN SELECT * from user u LEFT JOIN user2 u2 on u.id = u2.id;-- 3.681s 3.770s 3.650s EXPLAIN SELECT * from user2 u2 LEFT JOIN user u on u.id = u2.id;-- 0.002s 0.002s 0.003s -- 連接字段為非索引列 EXPLAIN SELECT * from user u LEFT JOIN user2 u2 on u.name = u2.name;-- 124.450s 139.875s 142.904s EXPLAIN SELECT * from user2 u2 LEFT JOIN user u on u.name = u2.name;-- 140.093s 142.917s 139.737s
執行計划結果(按上面的sql依次執行)
從執行結果中紅框標注看,可以得知為什么小表join大表比較快,這是因為小表u2作為驅動表只大概掃描了1000行,而大表u作為驅動表大概掃描了995757行。從紅框標注與藍框標注對比可以得知為什么join中的連接條件使用索引字段比非索引字段要快,首先前者比后者掃描的行數要少,其次我們注意到后者在Extra中明確表示用到了join的BNL算法(Block Nested Loop)°?從第3節的Block Nested-Loop算法介紹上看,這種算法是把外層驅動表的一部分數據放到了join buffer中以減少驅動表的循環次數,但是從上圖中的第4個結果看,內層表也用到了這種算法——這是否為mysql在新版本做出的優化不得而知。到了這里,我們只能從實驗證實我們的優化結論是正確的,但是為什么小表join大表比大表join小表快,為什么join的連接字段使用索引字段比使用非索引字段快,為什么當join的連接字段為非索引字段時,大表Join小表與小表join大表的速度差不多?還需要我們學習第3節才能找到答案。
三、三種join算法
本文第1節說明了join優化結論,並在第2節進行驗證,如果要想搞懂優化結論的原理,則需搞明白mysql在join時的相關算法:
NLJ算法 Nested Loop Join(或Simple Nested-Loop Join):這種算法是最low的,你可以簡單理解為這種算法就是一個雙層for循環算法,在join匹配時循環次數是最多的,在5.6之前如果join字段為非索引字段,會采用這種join算法。
BNLJ算法 Block Nested-Loop Join:這是5.6之后,mysql替換NLJ的升級算法,所以升級之處就在於它把join的驅動表放到了內存buffer中,拿內存buffer中的數據批量與內層表數據匹配,從而減少了驅動表的循環(匹配)次數。
INLJ算法 index Nested Loop Join:這是當join字段為索引字段時,mysql采用的算法,這種算法讓驅動表不直接與內層表進行逐行匹配,而是與內層表的連接索引字段進行匹配,這樣就減少了內層表的循環(匹配)次數。
不論是Index Nested-Loop Join 還是 Block Nested-Loop Join 都是在Simple Nested-Loop Join的算法的基礎上 減少嵌套的循環次數, 不同的是 Index Nested-Loop Join 是通過索引的機制減少內層表的循環次數,Block Nested-Loop Join 是通過一次緩存多條數據批量匹配的方式來減少外層表的循環次數。
下面是這3種算法的詳細介紹:
Simple Nested-Loop Join(簡單的嵌套循環連接)
簡單來說嵌套循環連接算法就是一個雙層for 循環 ,通過循環外層表的行數據,逐個與內層表的所有行數據進行比較來獲取結果,當執行select * from user tb1 left join level tb2 on tb1.id=tb2.user_id
時,我們會按類似下面代碼的思路進行數據匹配:
整個匹配過程會如下圖:
特點:
Nested-Loop Join 簡單粗暴容易理解,就是通過雙層循環比較數據來獲得結果,但是這種算法顯然太過於粗魯,如果每個表有1萬條數據,那么對數據比較的次數=1萬 * 1萬 =1億次,很顯然這種查詢效率會非常慢。
當然mysql 肯定不會這么粗暴的去進行表的連接,所以就出現了后面的兩種對Nested-Loop Join 優化算法,在執行join 查詢時mysql 會根據情況選擇 后面的兩種優join優化算法的一種進行join查詢。
Index Nested-Loop Join(索引嵌套循環連接)
Index Nested-Loop Join其優化的思路 主要是為了減少內層表數據的匹配次數, 簡單來說Index Nested-Loop Join 就是通過外層表匹配條件 直接與內層表索引進行匹配,避免和內層表的每條記錄去進行比較, 這樣極大的減少了對內層表的匹配次數,從原來的匹配次數=外層表行數 * 內層表行數,變成了 外層表的行數 * 內層表索引的高度,極大的提升了 join的性能。
案例:
如SQL:select * from user tb1 left join level tb2 on tb1.id=tb2.user_id
當level 表的 user_id 為索引的時候執行過程會如下圖:
注意:使用Index Nested-Loop Join 算法的前提是匹配的字段必須建立了索引。
Block Nested-Loop Join(緩存塊嵌套循環連接)
Block Nested-Loop Join 其優化思路是減少外層表的循環次數,Block Nested-Loop Join 通過一次性緩存多條數據,把參與查詢的列緩存到join buffer 里,然后拿join buffer里的數據批量°?這里我不太理解為什么把驅動表的數據拿到緩存buffer中就能批量與內層表進行匹配,比如說join buffer一次性緩存了3條數據,則這3條數據只需與內層表匹配一次即可?與內層表的數據進行匹配,從而減少了外層循環的次數,當我們不使用Index Nested-Loop Join的時候,默認使用的是Block Nested-Loop Join。
案例:
如SQL:select * from user tb1 left join level tb2 on tb1.id=tb2.user_id
當level 表的 user_id 不為索引的時候執行過程會如下圖:
小結
好了,根據對mysql的join算法的理解,我們可以回答第2節最后提出的問題了:
1、為什么join的連接字段使用索引字段比使用非索引字段快?
因為采用了Index Nested-Loop Join算法,極大的減少了內層表的匹配次數。
2、為什么小表join大表比大表join小表快?
這里先討論Join字段為索引字段的情況,因為小表join大表更能顯著地減少外層驅動表的循環次數,比如在第2節的舉例,外層驅動表為100萬條數據,內層表為1000條數據。如果外層驅動表為大表,即使采用Block Nested-Loop Join算法,因為join buffer的大小總是有限的,最終外層驅動表還是需要接近10萬次循環;而用小表join大表的話,外層驅動表僅用了1000次左右的循環,再加上join字段為索引字段,用到了Index Nested-Loop Join算法,又極大的減少了內層大表的循環次數,所以join字段為索引字段+小表join大表結合起來的查詢速度非常快。
3、為什么當join的連接字段為非索引字段時,大表Join小表與小表join大表的速度差不多?
因為雖然說把小表作為驅動表能極大減少外層循環的次數,但是內層表為大表,由於連接字段為非索引字段,不能用Index Nested-Loop Join算法減少內層循環的次數,所以當join的連接字段為非索引字段時,兩種形式的區別不大。
注意:
1、使用Block Nested-Loop Join 算法需要開啟優化器管理配置的optimizer_switch的設置block_nested_loop為on 默認為開啟,如果關閉則使用Simple Nested-Loop Join 算法;
通過指令:Show variables like 'optimizer_switc%'; 查看配置
2、設置join buffer 的大小
通過join_buffer_size參數可設置join buffer的大小
指令:Show variables like 'join_buffer_size%';