join 查詢優化

在開發中每每會出現查詢多表聯查的狀況,那麼就會用到 join 查詢。html

Join查詢種類

爲了方便說明,先定義一個統一的表,下面再作例子。mysql

CREATE TABLE `t2` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;

表 t一、t2 結構相等,t2 1000條記錄, t1 100 條記錄,t1 的數據在 t2 上都有。算法

Index Nested-Loop Join(NLJ)

關於 NLJ 能夠從名字上直接看出,這是一個使用到索引的 join 查詢,正由於使用到索引,因此不須要用到臨時表。sql

例子

select * from t1 straight_join t2 on (t1.a=t2.a);數組

straight_join :至關於強制版的 inner join。由於在使用 inner join 或 join 鏈接時,優化器默認使用的都是小表做爲驅動表,可是若是排序或篩選條件列是被驅動表,那麼該列的索引就會用不到,好比 select * from a inner join b where a.id=b.aid order by a.id,若是a的集合比b大,那麼mysql就會以b爲驅動表,這個時候若是a.id有索引的話,那麼這個索引在 order by 排序時是不起效的(on 篩選時能夠用到,排序只能經過驅動表排序那麼就須要額外排序,甚至用到臨時表,很是消耗性能,而 straight_join 的做用就是強制讓左表做爲驅動表,固然使用前提是在事先知道優化器選擇的驅動表效率較低,而後才能夠考慮使用 straight_join 。這裏就是爲了防止優化器選擇錯誤的驅動表,固然,這裏使用 inner join 也是會以 t1 小表做爲基礎表。oop

執行過程性能

一、從表 t1 中讀入一行數據 R;
二、從數據行 R 中,取出 a 字段到表 t2 裏去查找;
三、取出表 t2 中知足條件的行,跟 R 組成一行,做爲結果集的一部分;
四、重複執行步驟 1 到 3,直到表 t1 的末尾循環結束。優化

整個過程步驟1 會遍歷 t1 全部的行,因此一共會掃描100行。 而步驟2 由於會用到索引,因此每次掃描一次,一共掃描 100 行。最後拼接在一塊兒,因此一共掃描 200 行。spa

公式線程

假設被驅動表的行數是 M。每次在被驅動表查一行數據,要先搜索索引 a,再搜索主鍵索引。每次搜索一棵樹近似複雜度是以 2 爲底的 M 的對數,記爲 log2M,因此在被驅動表上查一行的時間複雜度是 2*log2M。
假設驅動表的行數是 N,執行過程就要掃描驅動表 N 行,而後對於每一行,到被驅動表上匹配一次。所以整個執行過程,近似複雜度是 N + N*2*log2M。

由此看來,不考慮其餘因素的影響(好比上面straight_join 說到的狀況),NLJ 方式以小表做爲驅動表的效率會更高

 

Simple Nested-Loop Join

沒有用到索引。

例子

select * from t1 straight_join t2 on (t1.a=t2.b);

執行過程

首先由於 straight_join 的做用,仍是以 t1 爲驅動表。執行時仍是先從 t1 上取一條記錄取 t2 上尋找對應的記錄,可是由於 t2 的 b 列上沒有索引,因此在 t2 上執行的是全表掃描。因此掃描行數爲 100+100*1000。因此 Simple Nested-Loop Join 的效率很低,這種方式也是沒有被 MySQL 採用。

 

Block Nested-Loop Join(BNJ)

這種方式也是沒有用到索引,可是和上面一種的區別是在內存中實現的。主要思路是將驅動表加載到一個內存空間 join_buffer 中,而後從被驅動表上每次拿出一條記錄到 join_buffer 中找到符合條件的記錄,而後返回。join_buffer 大小可由參數 join_buffer_size 設定,默認值是 256k。在 explain 中出現 Block Nested Loop 說明使用的是 BNJ 算法,BNJ 執行效率比較低,因此應該避免。 

例子

select * from t1 straight_join t2 on (t1.a=t2.b);

執行過程

一、若是 join_buffer 空間足夠存儲 t1 全部記錄:

  1)把表 t1 的數據讀入線程內存 join_buffer 中,因爲咱們這個語句中寫的是 select *,所以是把整個表 t1 放入了內存;
  2)掃描表 t2,把表 t2 中的每一行取出來,跟 join_buffer 中的數據作對比,知足 join 條件的,做爲結果集的一部分返回。須要注意的是,由於 join_buffer 是無序數組,因此雖然 t1 的a 列有索引,在這一步尋找時也不會用到索引

 

二、若是 join_buffer 空間不能存儲 t1 的全部記錄。那麼就會分批來處理。

  1)掃描表 t1,順序讀取數據行放入 join_buffer 中,放完第 88 行 join_buffer 滿了,繼續第 2 步;
  2)掃描表 t2,把 t2 中的每一行取出來,跟 join_buffer 中的數據作對比,知足 join 條件的,做爲結果集的一部分返回;
  3)清空 join_buffer;
  4)繼續掃描表 t1,順序讀取最後的 12 行數據放入 join_buffer 中,繼續執行第 2 步。

 

結論:以上這兩種掃描的總行數都是同樣的。 一、將 t1掃描進 join_buffer 100行;二、t2 每行去 joiin_buffer 上進行全表掃描 100*t2總行數1000。因此總行數爲 100 +100*1000。和上面的 Simple Nested-Loop Join 方式掃描的行數同樣,可是由於使用的 join_buffer 是在內存中的,因此執行的速度會比 Simple Nested-Loop Join 快得多。

公式

假設,驅動表的數據行數是 N,須要分 K 段才能完成算法流程,被驅動表的數據行數是 M。注意,這裏的 K 不是常數,N 越大 K 就會越大,所以把 K 表示爲λ*N,顯然λ的取值範圍是 (0,1)。那麼掃描行數就是 N+λ*N*M。

由此能夠看出,驅動表參與篩選的記錄數越少,掃描的行數就越少,效率也就越高。也就是在不考慮其餘因素的影響下,以小表爲驅動表能夠提升 BNJ方式的執行效率

 

優化

Index Nested-Loop Join(NLJ)

NLJ 查詢過程當中會用到索引,因此查詢的效率會很快,可是其仍是有優化空間的,那就是 MySQL 5.6引入的 Batched Key Access(BKA) 算法。其原理就是經過 MRR 實現順序讀,由於以前的 NLJ 過程是每次拿一條記錄去匹配,而後獲得對應的一條記錄,這樣每次獲取的記錄主鍵頗有可能不是按順序去查詢的,同時屢次的查詢使得執行效率比較低(每次都須要從 B+樹的根節點開始查找匹配)。

MRR

MRR 會先將要查詢的記錄主鍵添加到 read_rnd_buffer中(若是放不下就分屢次進行),對 read_rnd_buffer 中的 id 進行遞增排序,而後再依次按 id 去查詢,通過 MRR 優化的執行就會走 B+ 樹的葉子節點,因此查詢效率提升。下面以 sql:select * from t1 where a>=1 and a<=100 爲例,其中 a 列有索引,看一下開啓 MRR 執行的流程圖:

開啓

設置:SET  @@optimizer_switch='mrr=on,mrr_cost_based=on';

相關參數:
當mrr=on,mrr_cost_based=on,則表示cost base的方式還選擇啓用MRR優化,當發現優化後的代價太高時就會不使用該項優化
當mrr=on,mrr_cost_based=off(官方文檔的說法,是如今的優化器策略,判斷消耗的時候,會更傾向於不使用 MRR,把 mrr_cost_based 設置爲 off,就是固定使用 MRR 了。),則表示老是開啓MRR優化。

 

若是查詢使用了 MRR 優化,那麼使用 explain 解析就會出現 Using MRR 的提示

BKA

使用 BKA 的過程:

仍是以上面的 select * from t1 straight_join t2 on (t1.a=t2.b); 爲例

首先將 t1 的篩選字段存入 join_buffer(若是存不下就分屢次執行),而後將存儲的字段值批量去 t2 上匹配,獲得匹配的主鍵,而後進行主鍵排序,再去依次查詢對應的記錄返回。

 

Block Nested-Loop Join(BNJ)

BNJ 形成性能損失很高,主要緣由有如下三個方面:

一、可能會屢次掃描被驅動表,佔用磁盤 IO 資源;

二、判斷 join 條件須要執行 M*N 次對比(M、N 分別是兩張表的行數),若是是大表就會佔用很是多的 CPU 資源;

三、可能會致使 Buffer Pool 的熱數據被淘汰,影響內存命中率(影響嚴重)。經過 InnoDB 中的緩衝池(Buffer Pool) 能夠知道緩衝池是使用了 LRU 算法來對熱點數據進行了優化的,可是在某些狀況下仍是會出現熱點數據被擠掉的場景,使用 BNJ 進行屢次的查詢就是其中一種,由於 BNJ 操做若是涉及的表數據量比較大,那麼用到的數據也不少,那麼若是在使用到後面某一時刻某個會話也查詢了某個冷門數據,那麼由於以前 BNJ 也查詢了,而且中間的時間間隔達到了最大老年時間,因此這個冷門數據就會進入老年代頭部,擠掉其餘熱點數據。大表 join 操做雖然對 IO 有影響,可是在語句執行結束後,對 IO 的影響也就結束了。可是,對 Buffer Pool 的影響就是持續性的,須要依靠後續的查詢請求慢慢恢復內存命中率。

優化思路1:減小 BNJ 的循環次數,上面說到,屢次的掃描被驅動表會長時間佔用磁盤 IO 資源,形成系統總體性能降低。

方法:增大 join_buffer_size 的值,減小對被驅動表的掃描次數。

優化思路2:將 BNJ 優化成 NLJ。

方法1:在篩選條件字段使用率比較高時,能夠考慮爲其建立一個索引,這樣在執行時由於有索引就會變成 NLJ 了。

方法2:若是篩選字段使用率很低,爲其建立索引會提升維護的成本,作到得不償失,那麼該如何優化?答案是可使用臨時表,從 MySQL 中的臨時表 能夠知道,臨時表會隨着會話的結束而自動銷燬,省去了維護的成本;同時不一樣會話能夠建立同名的臨時表,不會產生衝突。這使得臨時表成爲優化篩選字段使用率低的 BNJ 查詢的絕佳方法。

  例:假設有表 t一、t2,表 t1 裏,插入了 1000 行數據, t2 中插入了 100 萬行數據 。執行 select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000; b 列使用率很低

  未優化前執行

  一、把表 t1 的全部字段取出來,存入 join_buffer 中。這個表只有 1000 行,join_buffer_size 默認值是 256k,能夠徹底存入。
  二、掃描表 t2,取出每一行數據跟 join_buffer 中的數據進行對比,
    1)若是不知足 t1.b=t2.b,則跳過;
    2)若是知足 t1.b=t2.b, 再判斷其餘條件,也就是是否知足 t2.b 處於[1,2000]的條件,若是是,就做爲結果集的一部分返回,不然跳過。整個篩選過程一共掃描了 1000*1000000 = 10億行。

  優化思路

  一、把表 t2 中知足條件的數據放在臨時表 tmp_t 中;
  二、爲了讓 join 使用 BKA 算法,給臨時表 tmp_t 的字段 b 加上索引;
  三、讓表 t1 和 tmp_t 作 join 操做。

  實現

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

  過程消耗:

  一、執行 insert 語句構造 temp_t 表並插入數據的過程當中,對錶 t2 作了全表掃描,這裏掃描行數是 100 萬。
  二、以後的 join 語句,掃描表 t1,這裏的掃描行數是 1000;join 比較過程當中,作了 1000 次帶索引的查詢(由於t1 1000行,做爲驅動表,t2做爲被驅動表)。相比於優化前的 join 語句須要作 10 億次條件判斷來講,這個優化效果仍是很明顯的。

  進一步優化

  臨時表又分爲磁盤臨時表和內存臨時表,使用內存臨時表效率比磁盤臨時表高,上面的引擎是 innodb,也就是磁盤臨時表,若是換成 Memory 引擎就是內存臨時表。可是相對的內存臨時表只能存儲2000行數據,因此在數據量特別大時仍是應該使用磁盤臨時表。

三張表優化

表結構:

CREATE TABLE `t1` (
 `id` int(11) NOT NULL,
 `a` int(11) DEFAULT NULL,
 `b` int(11) DEFAULT NULL,
 `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
create table t2 like t1;
create table t3 like t2;
insert into ... //初始化三張表的數據

如何優化語句:select * from t1 join t2 on(t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c>=X and t2.c>=Y and t3.c>=Z;

答:首先根據where 三個條件來判斷哪一個表符合條件的返回的字段長度最小,將最小的做爲驅動表。
一、第一種狀況,若是選出來是表 t1 或者 t3,那剩下的部分就固定了。(由於 join 順序是 t一、t二、t3,肯定小表直接向另外一個方向驅動就能夠了)
  1)若是驅動表是 t1,則鏈接順序是 t1->t2->t3,要在被驅動表字段建立上索引,也就是 t2.a 和 t3.b 上建立索引;
  2)若是驅動表是 t3,則鏈接順序是 t3->t2->t1,須要在 t2.b 和 t1.a 上建立索引。
同時,咱們還須要在第一個驅動表的字段 c 上建立索引。
二、第二種狀況是,若是選出來的第一個驅動表是表 t2 的話,則須要評估另外兩個條件的過濾效果。

 

總之,總體的思路就是,儘可能讓每一次參與 join 的驅動表的數據集,越小越好,由於這樣咱們的驅動表就會越小。

 

總結

NLJ 本來是不須要用到 join_buffer 的,可是能夠經過 BKA 優化使用 join_buffer ,此時方向是使用在 join_buffer 中的驅動表數據去被驅動表上匹配,而後獲得主鍵,排序、回表返回結果,若是 read_rnd_buffer 或者 join_buffer 空間不夠就分屢次進行。

BNL 本來沒有用到索引,因此必須使用 join_buffer 來幫助查詢,方向是被驅動表到 join_buffer 上的驅動表數據進行匹配,優化後變成 BKA 算法的 NLJ,因此方向也就變成了使用在 join_buffer 中的驅動表數據去被驅動表上匹配。因此在 BNL 優化前的思路就是減小被驅動表的遍歷次數,也就是增大 join_buffer 的大小;而優化後就須要在被驅動表上建立索引,來優化查詢。

 

join 的 on 條件與 where 條件的關聯

表結構:

create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);

on 條件寫在 where 中可能會使外鏈接失效

以上面的表結構,執行:

select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q1*/
select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/

執行結果:

分析

Q1:

解析:

由於是以 a 做爲驅動表,而 a 的 f1有索引,f2沒有索引,因此會用到臨時表來篩選,也就出現 using join buffer(Block hested Loop)

過程

一、把表 a 的內容讀入 join_buffer 中。由於是 select * ,因此字段 f1 和 f2 都被放入 join_buffer 了。
二、順序掃描表 b,對於每一行數據,判斷 join 條件(也就是 (a.f1=b.f1) and (a.f1=1))是否知足,知足條件的記錄, 做爲結果集的一行返回。若是語句中有 where 子句,須要先判斷 where 部分知足條件後,再返回。
三、表 b 掃描完成後,對於沒有被匹配的表 a 的行(在這個例子中就是 (1,1)、(2,2) 這兩行),把剩餘字段補上 NULL,再放入結果集中。

 

Q2:

解析:

爲何Q2執行會把 null 值部分過濾掉了?

這是由於在 where 條件中,NULL 跟任何值執行等值判斷和不等值判斷的結果,都是 NULL。這裏包括, select NULL = NULL 的結果,也是返回 NULL。因此在篩選時,先經過 on 判斷帶 null 的記錄,可是由於 where 條件的做用,會篩掉其中爲 null 的記錄,致使 left join 失效,因此優化器在實際執行時會將這條語句優化成 inner join,並把篩選條件移到 where 條件後面。整個語句就會被優化成下面的語句執行:

也就是 select a.f1, a.f2, b.f1, b.f2 from  a  join  b  where a.f1 = b.f1 and a.f2=b.f2

因此過程就變成:
順序掃描表 b,每一行用 b.f1 到表 a 中去查,匹配到記錄後判斷 a.f2=b.f2 是否知足(索引下推),知足條件的話就做爲結果集的一部分返回。

 

因此,若是想要執行外鏈接查詢,篩選條件就不能寫在 where 中。

 

內鏈接可能會將on條件優化成 where 條件

執行:

select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/
select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/

解析:

能夠看到兩條語句解析結果是如出一轍的,而且執行語句也與這兩條語句都不同,都被優化成 select * from a join b where a.f1=b.f1 and a.f2=b.f2 。執行過程和上面的 Q2 同樣。這是由於若是放在 on 中就會用到臨時表,效率會低一些,因此優化器直接優化放在 where 中配合索引下推經過索引一併完成判斷。

相關文章
相關標籤/搜索