Mysql優化之join優化


一 、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%';

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM