mysql join語句分析(一)


全表掃描

​ 假設,現在對一個200G的innodb的表,做全表掃描,把掃描結果保存在客戶端。

mysql ‑h$host ‑P$port ‑u$user ‑p$pwd ‑e "select * from db1.t" > $target_file

​ 這個語句的結果集存在哪里呢?

  • 實際上,服務端並不需要保存一個完整的結果集。取數據和發數據流程如下:
    1. 獲取一行,寫到net_buffer中。這塊內存的大小是由參數net_buffer_length定義的,默認是
      16k。
    2. 重復獲取行,直到net_buffer寫滿,調用網絡接口發出去。
    3. 如果發送成功,就清空net_buffer,然后繼續取下一行,並寫入net_buffer。
    4. 如果發送函數返回EAGAIN或WSAEWOULDBLOCK,就表示本地網絡棧(socket send
      buffer)寫滿了,進入等待。直到網絡棧重新可寫,再繼續發送。
  • 一個查詢在發送過程中,占用mysql內部的內存最大的就是net_buffer_length,並不會達到200G
  • socket send buffer 也不可能達到200G
  • 就是說,MySQL是“邊讀邊發的”,這個概念很重要。這就意味着,如果客戶端接收得慢,會
    導致MySQL服務端由於結果發不出去,這個事務的執行時間變長。

全表掃描對innodb的影響

  • innodb內存的一個作用是保存更新的結果,再配合redo log,避免了隨機斜盤。
  • 內存的數據頁在Buffer Pool中,在wal里,buffer pool即起到了加速更新的作用,也有個作用是加速查詢。其依賴一個重要指標:內存命中率。
    • 執行 show engine innodb status 可以看到命中率
    • 如果所有查詢需要的數據頁都能夠直接從內存得到,那是最好的,對應的命中率就是100%。
      但,這在實際生產上是很難做到的。
    • InnoDB Buffer Pool的大小是由參數 innodb_buffer_pool_size確定的,一般建議設置成可用物理內存的60%~80%。
    • InnoDB內存管理用的是最近最少使用 (Least Recently Used, LRU)算法,這個算法的核心就是淘汰最久未使用的數據。
  • 全表掃描還是比較耗費IO資源的,所以業務高峰期還是不能直接在線上主庫執行全表掃描的。

mysql如何使用join

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;

可以看到,這兩個表都有一個主鍵索引id和一個索引a,字段b上無索引。假設表t2有1000行數據,表t1是100行數據。

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

    straight_join可以讓mysql使用固定的連接方式執行join,這個語句里,ti是驅動表,t2是被驅動表。執行流程如下:

    • 從表t1讀入一行數據R
    • 從R中,取出a到t2里查找
    • 取出t2中滿足的行和R一行,作為結果集的一部分
    • 重復以上步驟,直到t1的循環結束。

    這個過程是先遍歷表t1,然后根據從表t1中取出的每行數據中的a值,去表t2中查找滿足條件的記
    錄。在形式上,這個過程就跟我們寫程序時的嵌套查詢類似,並且可以用上被驅動表的索引,所
    以我們稱之為“Index Nested-Loop Join”,簡稱NLJ。

    流程圖如下:

    t1做了全表掃描,100行,t2是走了樹搜索過程,也是100行。整個執行流程,總掃描行數是200。

  • 假設不適用join,用單表查詢。

    1. 執行select * from t1,查出表t1的所有數據,這里有100行;
    2. 循環遍歷這100行數據:
      • 從每一行R取出字段a的值$R.a;
      • 執行select * from t2 where a=$R.a;
      • 把返回的結果和R構成結果集的一行。

    可以看到,在這個查詢過程,也是掃描了200行,但是總共執行了101條語句,比直接join多了
    100次交互。除此之外,客戶端還要自己拼接SQL語句和結果。

如何選擇驅動表

  • 之前的join語句執行過程中,驅動表走的是全表掃描,被驅動表走樹搜索。
  • 假設被驅動表的行數是M,每次在被驅動表插一行數據,先搜索索引a,再搜索主鍵索引,每次搜索一次近似復雜度是logM,在被驅動表上查一行的時間復雜度是2*logM。
  • 假設驅動表行數是N,搜索過程就要掃描驅動表N行,對於每一行,到被驅動表匹配一次,因此整個過程復雜度近似N+N*2*logM,顯然,N對掃描行數的影響更大,應該讓小表做驅動表。
  • 小結:
    • 使用join語句,性能比強行拆成多個單表執行sql語句性能要好
    • 如果使用join語句,需要讓小表做驅動表。
    • 注意:前提是可以使用被驅動表的索引。

Simple Nested-Loop Join

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

由於表t2的字段b沒有索引,因此再用上圖的流程去匹配的時候,就要做一次全表掃描。

那么這一次掃描的行數就是100*1000 = 10萬行。這個就叫Simple Nested-Loop Join,

很笨重,下面是mysql使用的另一種算法Block Nested-Loop Join

Block Nested-Loop Join

  1. 把表t1的數據讀入線程內存join_buffer中,由於是select * ,因此是把整個表t1放入內存

  2. 掃描表t2,把t2中每一行取出來,和join_buffer中的數據做對比,滿足join條件的,作為結果集的一部分返回。

    所以,在整個過程中,只對t1和t2都做了一次全表掃描,總行數是1100,由於join_buffer是無序數組的方式組織的,因此對表t2中的每一行,都要做100次判斷,總共次數是10萬次,從時間復雜度來看,兩者是一樣的,但BNL的算法是內存上的操作,速度會更快,性能頁更好。

  • 如何選擇驅動表

    假設小標N行,大表M行,掃描的總行數是M+N,內存判斷次數是M*N,所以選擇大表還是小標,耗時是一樣的。

    如果t1是大表,join_buffer會不會放不下呢?

    join_buffer可以設置大小,默認是256k,如果放不下t1的所有數據,就分段放。流程如下:

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

    所以這個算法體現了BLOCK,表示分塊去join,判讀的總次數是不變的。再看一下驅動表的選擇問題。比如100*1000 = (88+12)*1000,分塊越多,被驅動表掃描次數也就越多。

    驅動表的數據行數是N,需要分K段才能完成算法流程,被驅動表的數據行數是M。

    這里的K不是常數,N越大K就會越大,因此把K表示為λ*N,顯然λ的取值范圍是(0,1)。

    1. 掃描行數是 N+λ*N*M;
    2. 內存判斷 N*M次。

    內存判斷次數是不受選擇哪個表作為驅動表影響的。而考慮到掃描行數,在M和N大小確定的情況下,N小一些,整個算式的結果會更小。所以結論是,應該讓小表當驅動表。

  • 小表的定義

    我們前面的例子是沒有加條件的。如果我在語句的where條件加上 t2.id<=50這個限定條件,再來看下這兩條語句:

    select * from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=50;
    select * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;
    

    如果是用第二個語句的話,join_buffer只需要放入t2的前50行,顯然是更好的。所以這里,“t2的前50行”是那個相對小的表,也就是“小表”。

    select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;
    select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;
    

    這個例子里,表t1 和 t2都是只有100行參加join。但是,這兩條語句每次查詢放入join_buffer中的數據是不一樣的:

    • 表t1只查字段b,因此如果把t1放到join_buffer中,則join_buffer中只需要放入b的值;
    • 表t2需要查所有的字段,因此如果把表t2放到join_buffer中的話,就需要放入三個字段id、a和b。我們應該選擇表t1作為驅動表。也就是說在這個例子里,“只需要一列參與join的表t1”是那個相對小的表。
    • 在決定哪個表做驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完
      成之后,計算參與join的各個字段的總數據量,數據量小的那個表,就是“小表”,應該作為驅動表。


免責聲明!

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



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