mysql Index Nested-Loop Join
阿新 • • 發佈:2019-01-14
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
語句:
其中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演算法的虛擬碼:
可以看到相比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的官方手冊中是這樣記錄的:
可以發現Join Buffer不是快取外表的整行記錄,但是columns of interest具體指的又是什麼?Inside君的第一反應是Join的列。為此,Inside君又去查了下mysql internals,查詢得到的說明如下所示:
used columns還是非常模糊。為此,Inside君詢問了好友李海翔,也是官方MySQL優化器團隊的成員,他答覆我的結果是:
“所有參與查詢的列”都會儲存到Join Buffer,而不是隻有Join的列
。最後,Inside君除錯了MySQL,在sql_join_buffer.cc檔案中驗證了這個結果。
比如下面的SQL語句,假設沒有索引,需要使用到Join Buffer進行連結:
假設上述SQL語句的外表是a,內表是b,那麼存放在Join Buffer中的列是所有參與查詢的列,在這裡就是(a.col1,a.col2,a.col3)。
通過上面的介紹,我們現在可以得到內表的掃描次數為:
對於有經驗的DBA就可以預估需要分配的Join Buffer大小,然後儘量使得內表的掃描次數儘可能的少,最優的情況是隻掃描內表一次。
Join Buffer的分配
需要牢記的是, Join Buffer是在Join之前就進行分配 ,並且每次Join就需要分配一次Join Buffer,所以假設有N張表參與Join,每張表之間通過Block Nested-Loop Join,那麼總共需要分配N-1個Join Buffer,這個記憶體容量是需要DBA進行考量的。 Join Buffer可分為以下兩類:
Block Nested-Loop Join總結
Block Nested-Loop Join極大的避免了內表的掃描次數,如果Join Buffer可以快取外表的資料,那麼內表的掃描僅需一次,這和Hash Join非常類似。但是Block Nested-Loop Join依然沒有解決的是Join比較的次數,其仍然通過Join判斷式進行比較。綜上所述,到目前為止各Join演算法的成本比較如下所示:
Index Nested-Loop Join
(接上篇,公眾賬號回覆 join 可見)由於訪問的是輔助索引,如果查詢需要訪問聚集索引上的列,那麼必要需要進行回表取資料,看似每條記錄只是多了一次回表操作,但這才是 INLJ 演算法最大的弊端 。首先,輔助索引的 index lookup 是比較隨機 I/O 訪問操作。其次,根據
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跑跑看。
接著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> |
可以看到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. |
We only store the used columns in the join buffer, not the whole rows. |
SELECT a.col3 FROM a,b WHERE a.col1 = b.col2 AND a.col2 > …. AND b.col2 = … |
Scaninner_table = (Rn * used_column_size) / join_buffer_size + 1 |
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
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 |