在我們的測試過程中,發現hash join 在特殊情況下, 存在笛卡爾積關聯的現象,於是我們對這個現象進行更進一步地探究,以便找出其中的原因。 為了便於模擬以及觀察現象。我們準備3個非常簡單的表以及相應的數據,來進行本次實驗,表跟數據如下:
create table t1 ( t1_id varchar(20),t1_name varchar(20) ,t1_addr varchar(20));
create table t2 ( t2_id varchar(20),t2_name varchar(20) ,t2_addr varchar(20));
create table t3 ( t3_id varchar(20),t3_name varchar(20) ,t3_addr varchar(20));
T1表:T1表總數1000行。
insert into t1 values('a0','name0','addr0');
insert into t1 values('a1','name1','addr1');
insert into t1 values('a2','name2','addr2');
insert into t1 values('a3','name3','addr3');
insert into t1 values('a4','name4','addr4');
.....................................................................
....................................................................
insert into t1 values('a997','name997','addr997');
insert into t1 values('a998','name998','addr998');
insert into t1 values('a999','name999','addr999');
T2表: T2表總數50000行。
insert into t2 values('a0','name0','addr0');
insert into t2 values('a1','name1','addr1');
insert into t2 values('a2','name2','addr2');
insert into t2 values('a3','name3','addr3');
........................................................................
................................................................................
insert into t2 values('a49998','name49998','addr49998');
insert into t2 values('a49999','name49999','addr49999');
T3表:T3表的總數是5100行。
insert into t3 values('a0','name0','addr0');
insert into t3 values('a1','name1','addr1');
insert into t3 values('a2','name2','addr2');
..................................................................
..........................................................
insert into t3 values('a5097','name5097','addr5097');
insert into t3 values('a5098','name5098','addr5098');
insert into t3 values('a5099','name5099','addr5099');
為了勾起繼續讀下去興趣,這裡拋出一個疑問:在實驗準備中, T1表造數1000行,T2表造數50000行,T3表為啥造數5100行,而不是5000行?5100跟5000有區別嗎?繼續讀下去, 5000跟5100這兩個數字的差別,含有這篇文章的本質內容,本文要講得就是這個。
然後執行下面這個SQL, 執行時間1.30秒。說明一下: 這個資料庫是debug模式安裝的,且跑在自己的虛擬機上, 所以比較慢。正常情況,這點數據量做正常地hash jion ,要不了1.3秒。
mysql> select count(*) from t1,t2,t3 where t1_id=t2_id and t2_addr=t3_addr;
++
| count(*) |
++
| 1000 |
++
1 row in set (1.30 sec)
我們看看執行計劃:
表的關聯順序是T1->T2->T3,這是一個非常正常的關聯順序,也是我們想要的關聯順序。
現在我們回到前面提的問題:」T3表為什麼造數5100行,而不是5000?」。按照我們習慣性的觀點,表的數量越少,sql執行得越快,現在我們的表T3當前有數據5100行,我們來刪除100行記錄, 讓它變成5000行,我們來觀察情況是否有變? 按照習慣性的觀點推理,SQL執行速度至少不會變慢。 因為表的數據量少了,而且減少得也不多,就100行,執行計劃變化的可能性也不大。
mysql> delete from t3 order by t3_id desc limit 100;
Query OK, 100 rows affected (0.44 sec)
mysql> select count(*) from t3;
++
| count(*) |
++
| 5000 |
++
1 row in set (0.37 sec)
然後執行之前的SQL,觀察是否發生些什麼?
mysql> select count(*) from t1,t2,t3 where t1_id=t2_id and t2_addr=t3_addr;
++
| count(*) |
++
| 900 |
++
1 row in set (30.87 sec)
這個SQL的執行時間從1.3秒變成了30秒,因為我們刪除了100行的數據,SQL的執行速度反而下降了,那一定是執行計劃變了,變成了什麼樣子呢?
這次表的關聯順序變成了T1->T3->T2, 但T1跟T3之間,在這個示列SQL中,T1跟T3本身沒有直接地關聯關係,所以造成了笛卡爾積關聯,關聯後將產生1000 * 5000 條臨時記錄,這就是SQL執行時間從1.3秒變成30秒的直接原因。
T3表的數量從5100行變成5000行,導致SQL的執行計劃發生了變化。 對數據進一步變化是否會對執行計劃產生影響,有興趣實驗的話,可以自己有空的時候動手測試一下。例如將T3表的數量從5000繼續往下降,或者從5100繼續往上漲 , 看看是否會導致執行計劃的改變。
以上是實驗現象, 現在來透過現象去了解背後的原因,作者將目前了解的原理跟大家描述一下(註:特別精簡版,僅描述示列SQL的關鍵執行路徑)。
1. 首先優化器在編排表的關聯順序之前,先把要關聯的表進行排序,有依賴關係的話需要優先確定表的依賴關係,如外連接中的依賴關係,嵌套關聯中ref訪問等,依賴關係影響排序位置。如果沒有依賴關係,按照表的數量進行排序,因為T1,T2,T3表之間的關聯,不是外連接,也沒有索引,不可能存在ref類型的訪問,所以按照表的數量排定順序,順序是t1->t3->t2. 我們把這個順序命名為tab_order. 具體實現這個邏輯的代碼如下:
if (jt1->found_records > jt2->found_records) return false;
if (jt1->found_records < jt2->found_records) return true;
該順序是第一次排序,非最終join順序
下面是這個Join_tab_compare_default函數的註解:
2. 然後優化器才進行表關聯順序的編排,規則如下:
因為通過前面的計算,按照表的行數的多少排定的tab_order順序是t1->t3->t2 。所以,首先取出T1表,然後確定T1表的訪問方式,因為沒有索引,只能全表掃描,掃描成本計算方式如下:
IO成本+取數據的成本(通常也稱CPU成本)。 因為表很小,IO成本比較小,主要是取數成本。在通過執行explain format=tree命令 的輸出中,顯示T1的cost為101.25. 由表的行數1000*0.100000000....1(我們把它當成0.1後,也就是100) 計算得出取數成本100(cpu成本), 再加上IO成本1, 總共101.
接下來,T1表選擇先計算跟T3表關聯的成本(因為在前面提到的tab_order順序中,順序為T1->T3->T2),因為表T1 跟T3之間沒有關聯關係,所以做笛卡爾積關聯,T3跟T1關聯後的結果是5000*1000,也就是500萬,取數成本為50萬,再加上表T1,表T3的IO成本,總成本就是執行計劃圖中顯示的500105.88。(因為IO成本佔比實在太小,後面在計算成本時,作者可能會直接忽略這部分)。
然後,再跟T2表進行關聯,關聯成本計算完畢之後,第一次得出這三個表之間關聯的全路徑T1->T3->T2的關聯成本,方法同前,因為表T1跟T3表笛卡爾積關聯,關聯後記錄數為500萬,表T2的總數為50000, 所以取數成本就是500百萬*5萬*0.1係數=250億。我們再來看上面出現過的T1->T3->T2執行計劃圖,完整路徑的總代價為251億(有差別是因為浮點計算,還有係數按照0.1計算,而不是0.1........1)。
在第一次全路徑計算完成之後,再回退到上一層(執行計劃生成函數用得是遞歸算法,到頂之後回退),於是選擇T2表跟T1表進行關聯,這一步計算完成之後,可能就決定了T1表到底跟T2表關聯,還是跟T3表進行關聯。(為什麼說可能,因為可能會進行全路徑成本的比較,也可能不需要,這就是默認的優化器「偷懶」的操作,規則是局部成本最優。如果T1->T2的關聯成本優於T1->T3,則繼續計算全路徑T1->T2->T3三表關聯的成本,如果T1->T2的關聯成比T1->T3高,直接拋棄,不再擴展這條線路上的路徑)。
下面我們來計算T1->T2的關聯成本,T1表1000行,T2表50000行,笛卡爾積之後是5000萬,但是T1跟T2之間有等於條件,所以會產生過濾,過濾效果怎麼計算? 因為表T2的T2_id沒有索引,怎麼去估算這個T1_id=T2_id條件的過濾效果? 所以問題就出現在這裡, MySQL對無法估算過濾效果的過濾條件,給一個統一的默認值,這個默認值就是0.1f. 所以MYSQL優化器認為T1跟T2關聯後會產生比500萬條還多一點點的記錄(浮點計算的原因),認為比T1表跟T3表做笛卡爾積關聯後的數量多(T1跟T3表關聯後是500萬,看到這裡,可能會明白這個實驗案例中T3表的數據量是精準設計的),所以優化器選擇了將T1表與T3表進行笛卡爾積的關聯。因此,如果T3的數量再大一些,表T1跟表T3關聯後的笛卡爾積的數量將超過500萬, 那優化器將選擇T1表跟T2表進行關聯。 但實際上, T1表跟T2表關聯後的記錄才1000條, 因為優化器沒有辦法知道關聯後到底會有多少條記錄,所以給了一個默認值0.1係數,按照十分一的比例進行過濾。
上面只是理論分析了T1表跟T2表關聯的成本,我們來看一下T1跟T2表關聯的實際成本,這次需要通過關鍵字straight_join強制指定關聯順序,不然優化器會選擇T1跟T3表做笛卡爾積關聯。
explain format=tree select count(*) from t1 straight_join t2 straight_join t3 where t1_id=t2_id and t2_addr=t3_addr;
表的關聯順序為T1->T2->T3, T1跟T2表關聯後的成本為:
cost=5023259.16, rows=5023100
而按照T1->T3->T2關聯順序進行關聯, 則如下(這個圖片在前面出現過,現重新再拿來對比)
表T1->T3關聯後的成本為:
cost=500105.88, rows=5000000
將這個兩個執行計劃的成本進行比較:
cost比較:500105.88<5023259.16, 行數比較:5000000<5023100 , 所以優化器選擇了T1->T3表進行笛卡爾積關聯。因為這兩個不同路徑(或者方案)的執行成本已經非常接近,所以當T3表再增大一點點,在本次實驗案例中,是將其數量從5000,變成5100之後, 優化器將選擇T1表將跟T2表進行關聯,這樣就出現了表的數量增加了,執行速度反而變快了的「非常規現象」。