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算法的成本比較以下所示:

未完待續......
相關文章
相關標籤/搜索