正文
Inside君發現很少有人能夠完成講明白My
SQL的Join類型與
算法,網上流傳着的要提升Join性能,加大變量join_buffer_size的謬論更是隨處可見。當然,也有一些無知的PGer攻擊MySQL不支持Hash Join,所以不適合一些分析類的操作。MySQL的確不支持Hash Join,也不支持Sort Merge Join,但是MySQL在Join上也有自己的獨特的優化與處理,此外,分支版本MariaDB已支持Hash Join,因此拿MySQL來做一些“簡單”的分析查詢也是完全能夠接受的。當然,如果數據量真的上去了,那么即使支持Hash Join的傳統MPP架構的關系型數據庫可能也是不合適的,這類分析查詢或許應該交給更為專業的
Hadoop集群來計算。
Join的成本
在講述MySQL的Join類型與算法前,看看兩張表的Join的過程:
上圖的Fetch階段是指當內表關聯的列是輔助索引時,但是需要訪問表中的數據,那么這時就需要再訪問主鍵索引才能得到數據的過程,不論表的存儲引擎是InnoDB存儲引擎還是MyISAM,這都是無法避免的,只是MyISAM的回表速度要快點,因為其輔助索引存放的就是指向記錄的指針,而InnoDB存儲引擎是索引組織表,需要再次通過索引查找才能定位數據。
Fetch階段也不是必須存在的,如果是聚集索引鏈接,那么直接就能得到數據,無需回表,也就沒有Fetch這個階段。另外,上述給出了兩張表之間的Join過程,多張表的Join就是繼續上述這個過程。
接着計算兩張表Join的成本,這里有下列幾種概念:
外表的掃描次數,記為O。通常外表的掃描次數都是1,即Join時掃描一次驅動表的數據即可
內表的掃描次數,記為I。根據不同Join算法,內表的掃描次數不同
讀取表的記錄數,記為R。根據不同Join算法,讀取記錄的數量可能不同
Join的比較次數,記為M。根據不同Join算法,比較次數不同
回表的讀取記錄的數,記為F。若Join的是輔助索引,可能需要回表取得最終的數據
評判一個Join算法是否優劣,就是查看上述這些操作的開銷是否比較小。當然,這還要考慮I/O的訪問方式,順序還是隨機,總之Join的調優也是門藝術,並非想象的那么簡單。
Simple Nested-Loop Join
網上大部分說MySQL只支持Nested-Loop Join,故性能差。但是Nested-Loop join一定差嗎?Hash Join比Nested-Loop Join強?Inside君感覺這樣的理解都是片面的,Hash Join可能僅是Nested-Loop Join的一種變種。所以Inside君打算從算法的角度來分析MySQL支持的Join,並以此分析對於Join語句的優化。
首先來看Simple Nested-Loop Join(以下簡稱SNLJ),也就是最朴素的Nested-Loop Join,其算法偽代碼如下所示:
For each row r in R do
Foreach row s in S do
If r and s satisfy the join condition
Then output the tuple
下圖能更好地顯示整個SNLJ的過程:
SNLJ的算法相當簡單、直接。即外表(驅動表)中的每一條記錄與內表中的記錄進行判斷。但是這個算法也是相當粗暴的,粗暴的原因在於這個算法的開銷其實非常大。假設外表的記錄數為R,內表的記錄數位S,根據上一節Inside君對於Join算法的評判標准來看,SNLJ的開銷如下表所示:
可以看到讀取記錄數的成本和比較次數的成本都是S*R,也就是笛卡兒積。假設外表內表都是1萬條記錄,那么其讀取的記錄數量和Join的比較次數都需要上億。這樣的算法開銷,Inside君也只能:呵呵。
Index Nested-Loop Join
SNLJ算法雖然簡單明了,但是也是相當的粗暴。因此,在Join的優化時候,通常都會建議在內表建立索引,以此降低Nested-Loop Join算法的開銷,MySQL數據庫中使用較多的就是這種算法,以下稱為INLJ。來看這種算法的偽代碼:
For each row r in R do
lookupr in S index
if found s == r
Then output the tuple
由於內表上有索引,所以比較的時候不再需要一條條記錄進行比較,而可以通過索引來減少比較,從而加速查詢。整個過程如下圖所示:
可以看到外表中的每條記錄通過內表的索引進行訪問,因為索引查詢的成本是比較固定的,故優化器都傾向於使用記錄數少的表作為外表(這里是否又會存在潛在的問題呢?)。故INLJ的算法成本如下表所示:
上表Smatch表示通過索引找到匹配的記錄數量。同時可以發現,通過索引可以大幅降低內表的Join的比較次數,每次比較1條外表的記錄,其實就是一次indexlookup(索引查找),而每次index lookup的成本就是樹的高度,即IndexHeight。
INLJ的算法並不復雜,也算簡單易懂。但是效率是否能達到用戶的預期呢?其實如果是通過表的主鍵索引進行Join,即使是大數據量的情況下,INLJ的效率亦是相當不錯的。因為索引查找的開銷非常小,並且訪問模式也是順序的(假設大多數聚集索引的訪問都是比較順序的)。
大部分人詬病MySQL的INLJ慢,主要是因為在進行Join的時候可能用到的索引並不是主鍵的聚集索引,而是輔助索引,這時INLJ的過程又需要多一步Fetch的過程,而且這個過程開銷會相當的大:
由於訪問的是輔助索引,如果查詢需要訪問聚集索引上的列,那么必要需要進行回表取數據,看似每條記錄只是多了一次回表操作,但這才是
由於訪問的是輔助索引,如果查詢需要訪問聚集索引上的列,那么必要需要進行回表取數據,看似每條記錄只是多了一次回表操作,但這才是INLJ算法最大的弊端。首先,輔助索引的index lookup是比較隨機I/O訪問操作。其次,根據index lookup再進行回表又是一個隨機的I/O操作。所以說,INLJ最大的弊端是其可能需要大量的離散操作,這在SSD出現之前是最大的瓶頸。而即使SSD的出現大幅提升了隨機的訪問性能,但是對比順序I/O,其還是慢了很多,依然不在一個數量級上。