MySQL Join算法與調優白皮書(二)


Index Nested-Loop 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
 
可以看到相比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算法的成本比較如下所示:

未完待續......


免責聲明!

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



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