1. 程式人生 > >mysql Index Nested-Loop Join

mysql Index Nested-Loop Join

MySQL mysql Index Nested-Loop Join 原創 2015-06-17 姜承堯 InsideMySQL InsideMySQL

Index Nested-Loop Join
(接上篇,公眾賬號回覆 join 可見)由於訪問的是輔助索引,如果查詢需要訪問聚集索引上的列,那麼必要需要進行回表取資料,看似每條記錄只是多了一次回表操作,但這才是 INLJ 演算法最大的弊端 。首先,輔助索引的 index lookup 是比較隨機 I/O 訪問操作。其次,根據
index lookup 再進行回表又是一個隨機的 I/O 操作。所以說, INLJ 最大的弊端是其可能需要大量的離散操作,這在 SSD 出現之前是最大的瓶頸。而即使 SSD 的出現大幅提升了隨機的訪問效能,但是對比順序 I/O ,其還是慢了很多,依然不在一個數量級上。例如下面的這個 SQL 語句:
SELECT
COUNT(*)
FROM
part,
lineitem
WHERE
l_partkey = p_partkey
AND p_retailprice > 2050
AND l_discount > 0.04;



其中p_partkey是表part的主鍵,l_partkey是表lineitem的一個輔助索引,由於表part資料較小,因此作為外表(驅動表)。但是內表Join完成後還需要判斷條件l_discount > 0.04,這個在聚集索引上,故需要回表進行讀取。根據explain得到上述SQL的執行計劃如下圖所示:


Block Nested-Loop Join
演算法說明
在有索引的情況下,MySQL會嘗試去使用Index Nested-Loop Join演算法,在有些情況下,可能Join的列就是沒有索引,那麼這時MySQL的選擇絕對不會是最先介紹的Simple Nested-Loop Join演算法,因為那個演算法太粗暴,不忍直視。資料量大些的複雜SQL估計幾年都可能跑不出結果,如果你不信,那就是too young too simple。或者Inside君可以給你些SQL跑跑看。
Simple Nested-Loop Join演算法的缺點在於其對於內表的掃描次數太多,從而導致掃描的記錄太過龐大。Block Nested-Loop Join演算法較Simple Nested-Loop Join的改進就在於可以減少內表的掃描次數,甚至可以和Hash Join演算法一樣,僅需掃描內表一次。
接著Inside君帶你來看看Block Nested-Loop Join演算法的虛擬碼:
For each tuple r in R do
store used columns as p from R in join buffer
For each tuple s in S do
If p and s satisfy the join condition
Then output the tuple <p,s>
可以看到相比Simple Nested-Loop Join演算法,Block Nested-LoopJoin演算法僅多了一個所謂的Join Buffer,然為什麼這樣就能減少內表的掃描次數呢?下圖相比更好地解釋了Block Nested-Loop Join演算法的執行過程:



可以看到Join Buffer用以快取連結需要的列,然後以Join Buffer批量的形式和內表中的資料進行連結比較。就上圖來看,記錄r1,r2 … rT的連結僅需掃內表一次,如果join buffer可以快取所有的外表列,那麼連結僅需掃描內外表各一次,從而大幅提升Join的效能。
Join Buffer
變數join_buffer_size
從上一節中可以發現Join Buffer是用來減少內表掃描次數的一種優化,但Join Buffer又沒那麼簡單,在上一節中Inside君故意忽略了一些實現。 首先變數join_buffer_size用來控制Join Buffer的大小,調大後可以避免多次的內表掃描,從而提高效能。也就是說,當MySQL的Join有使用到Block Nested-Loop Join,那麼調大變數join_buffer_size才是有意義的。而前面的Index Nested-Loop Join如果僅使用索引進行Join,那麼調大這個變數則毫無意義。 變數join_buffer_size的預設值是256K,顯然對於稍複雜的SQL是不夠用的。好在這個是會話級別的變數,可以在執行前進行擴充套件。Inside君建議在會話級別進行設定,而不是全域性設定,因為很難給一個通用值去衡量。另外,這個記憶體是會話級別分配的,如果設定不好容易導致因無法分配記憶體而導致的宕機問題。
需要特別注意的是,變數join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而之後的版本才能在64位作業系統下申請大於4G的Join Buffer空間。
Join Buffer快取的物件
Join Buffer快取的物件是什麼,這個問題相當關鍵和重要。然在MySQL的官方手冊中是這樣記錄的:
Only columns of interest to the join are stored in the join buffer, not whole rows.
可以發現Join Buffer不是快取外表的整行記錄,但是columns of interest具體指的又是什麼?Inside君的第一反應是Join的列。為此,Inside君又去查了下mysql internals,查詢得到的說明如下所示:
We only store the used columns in the join buffer, not the whole rows.
used columns還是非常模糊。為此,Inside君詢問了好友李海翔,也是官方MySQL優化器團隊的成員,他答覆我的結果是: “所有參與查詢的列”都會儲存到Join Buffer,而不是隻有Join的列 。最後,Inside君除錯了MySQL,在sql_join_buffer.cc檔案中驗證了這個結果。 比如下面的SQL語句,假設沒有索引,需要使用到Join Buffer進行連結:
SELECT a.col3 FROM a,b
WHERE a.col1 = b.col2
AND a.col2 > …. AND b.col2 = …
假設上述SQL語句的外表是a,內表是b,那麼存放在Join Buffer中的列是所有參與查詢的列,在這裡就是(a.col1,a.col2,a.col3)。 通過上面的介紹,我們現在可以得到內表的掃描次數為:
Scaninner_table = (Rn * used_column_size) / join_buffer_size + 1
對於有經驗的DBA就可以預估需要分配的Join Buffer大小,然後儘量使得內表的掃描次數儘可能的少,最優的情況是隻掃描內表一次。
Join Buffer的分配
需要牢記的是, Join Buffer是在Join之前就進行分配 ,並且每次Join就需要分配一次Join Buffer,所以假設有N張表參與Join,每張表之間通過Block Nested-Loop Join,那麼總共需要分配N-1個Join Buffer,這個記憶體容量是需要DBA進行考量的。 Join Buffer可分為以下兩類:
  • regular join buffer
  • incremental join buffer
regular join buffer是指Join Buffer快取所有參與查詢的列, 如果第一次使用Join Buffer,必然使用的是regular join buffer。 incremental join buffer中的Join Buffer快取的是當前使用的列, 以及之前使用Join Buffer的指標 。在多次進行Join的操作時,這樣可以極大減少Join Buffer對於記憶體開銷的需求。 此外,對於NULL型別的列,其實不需要存放在Join Buffer中,而對於VARCHAR型別的列,也是僅需最小的記憶體即可,而不是以CHAR型別在Join Buffer中儲存。最後,從MySQL 5.6版本開始,對於Outer Join也可以使用Join Buffer。
Block Nested-Loop Join總結
Block Nested-Loop Join極大的避免了內表的掃描次數,如果Join Buffer可以快取外表的資料,那麼內表的掃描僅需一次,這和Hash Join非常類似。但是Block Nested-Loop Join依然沒有解決的是Join比較的次數,其仍然通過Join判斷式進行比較。綜上所述,到目前為止各Join演算法的成本比較如下所示:
開銷統計 SNLJ INLJ BNLJ
外表掃描次數:O 1 1 1
內表掃描次數:I R 0 R*used_column_size/ join_buffer_size + 1
讀取記錄數:R R + S*R R + Smatch R + S*I
Join比較次數:M S*R R * IndexHeight S*R
回表讀取記錄次數:F 0 Smatch (if possible) 0