由一次Left Join查詢緩慢引出的Explain和Join算法詳解
前些日子在生產環境中,項目經理偶然發現有一條SQL執行的非常緩慢,達到了不殺死這個語句就難以平民憤的程度。於是委派我來解決這個問題。
后來追蹤到這是一個600萬條數據的表和一個700萬條數據的表 left join 的故事,sql語句類似於下面這種:
SELECT a.column_1, a.column_2, a.column_3, c.column_1 FROM table_1 a LEFT JOIN table_2 c ON a.column_1 = c.column_1 WHERE a.column_1 = 'value1'
AND a.column_2 BETWEEN STR_TO_DATE('2021-05-27 23:55:59','%Y-%m-%d %H:%i:%s')
AND STR_TO_DATE('2021-05-27 23:59:59','%Y-%m-%d %H:%i:%s') ORDER BY a.column_2 DESC LIMIT 0 , 1
我眉頭一皺,發現事情並不簡單,就想到了先用 explain 來看看這個語句到底干了什么。
Explain詳解
執行explain之后的結果如下所示:
id
- id越大執行優先級越高
- id相同則從上往下執行
- id為NULL最后執行
select_type
- SIMPLE:簡單SELECT,不使用UNION或子查詢等。
- PRIMARY:子查詢中最外層查詢,查詢中若包含任何復雜的子部分,最外層的select被標記為PRIMARY。
- UNION:UNION中的第二個或后面的SELECT語句。
- DEPENDENT UNION:在包含UNION或者UNION ALL的大查詢中,如果各個小查詢都依賴於外層查詢的話,那除了最左邊的那個小查詢之外,其余的小查詢的select_type的值就是DEPENDENT UNION。
- UNION RESULT:MySQL選擇使用臨時表來完成UNION查詢的去重工作,針對該臨時表的查詢的select_type就是UNION RESULT。
- SUBQUERY:如果包含子查詢的查詢語句不能夠轉為對應的semi-join的形式,並且該子查詢是不相關子查詢,並且查詢優化器決定采用將該子查詢物化的方案來執行該子查詢時,該子查詢的第一個SELECT關鍵字代表的那個查詢的select_type就是SUBQUERY。
- DEPENDENT SUBQUERY:如果包含子查詢的查詢語句不能夠轉為對應的semi-join的形式,並且該子查詢是相關子查詢,則該子查詢的第一個SELECT關鍵字代表的那個查詢的select_type就是DEPENDENT SUBQUERY。
- DERIVED:對於采用物化的方式執行的包含派生表的查詢,該派生表對應的子查詢的
select_type
就是DERIVED。 - UNCACHEABLE SUBQUERY:一個子查詢的結果不能被緩存,必須重新評估外鏈接的第一行。
- MATERIALIZED:當查詢優化器在執行包含子查詢的語句時,選擇將子查詢物化之后與外層查詢進行連接查詢時,該子查詢對應的select_type屬性就是MATERIALIZED。
semi-join:
是指當一張表在另一張表找到匹配的記錄之后,半連接(semi-jion)返回第一張表中的記錄。
與條件連接相反,即使在右節點中找到幾條匹配的記錄,左節點 的表也只會返回一條記錄。
另外,右節點的表一條記錄也不會返回。半連接通常使用IN 或 EXISTS 作為連接條件。
物化: 這個將子查詢結果集中的記錄保存到臨時表的過程稱之為物化(Materialize)。
那個存儲子查詢結果集的臨時表稱之為物化表。
正因為物化表中的記錄都建立了索引(基於內存的物化表有哈希索引,基於磁盤的有B+樹索引),通過索引執行IN語句判斷某個操作數在不在子查詢結果集中變得非常快,從而提升了子查詢語句的性能。
table
當前查詢的表名。表名有可能是原名,有可能是別名,要看SQL語句的具體情況。
partitions
如果數據表建立分區了的話,這里會顯示查詢用到的分區。
type
表示MySQL在表中找到所需行的方式,又稱“訪問類型”。
常用的類型有: ALL、index、range、 ref、eq_ref、const、system、NULL(從左到右,性能從差到好)
- ALL:Full Table Scan, MySQL將遍歷全表以找到匹配的行。
- index:Full Index Scan,index與ALL區別為index類型只遍歷索引樹。
- range:只檢索給定范圍的行,使用一個索引來選擇行。
- ref:表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值。
- eq_ref:類似ref,區別就在使用的索引是唯一索引,對於每個索引鍵值,表中只有一條記錄匹配,簡單來說,就是多表連接中使用 primary key 或者 unique key 作為關聯條件。
- const、system:當MySQL對查詢某部分進行優化,並轉換為一個常量時,使用這些類型訪問。如將主鍵置於where列表中,MySQL就能將該查詢轉換為一個常量,system是const類型的特例,當查詢的表只有一行的情況下,使用system。
- NULL:MySQL在優化過程中分解語句,執行時甚至不用訪問表或索引,例如從一個索引列里選取最小值可以通過單獨索引查找完成。
possible_keys
可能用到的索引。查詢涉及到的字段上若存在索引,則列出,但查詢的時候不一定會使用(如果沒有任何索引顯示 null)。
key
實際用到的索引。
key_len
表示索引中使用的字節數。
該列計算查詢中使用的索引的長度,在不損失精度的情況下,長度越短越好。如果鍵是NULL,則長度為NULL。
該字段顯示為索引字段的最大可能長度,並非實際使用長度。
ref
在key列顯示的索引中,表在查找時所用到的列或常量,常見的有:const(常量),字段名(例:t1.id)。
rows
預計要讀取並檢索的行數,注意這個不是結果集里的行數,並且這個數字是一個估值,不是准確值。
filtered
返回結果的行占需要讀到的行(rows列的值)的百分比。
Extra
- Using where:SQL使用了where條件過濾數據。(可以優化)
- Using index:SQL所需要返回的列都在一棵索引樹上,不需要訪問其對應的實際行記錄。(性能較好)
- Using index condition:SQL用到了索引,但不是所有需要返回的列都在索引上,還需要訪問實際的行記錄。(性能次於Using index)
- Using filesort:要得到想要的結果集,需要將所有的數據進行排序。在一個沒有建立索引的列上進行了order by,就會觸發filesort,常見的優化方案是,在order by的列上添加索引,避免每次查詢都全量排序。(性能很差,需要優化)
- Using temporary:需要建立臨時表來得到想要的結果集。group by和order by同時存在,且作用於不同的字段時,就會建立臨時表,以便計算出最終的結果集。(性能較差,需要優化)
- Using join buffer (Block Nested Loop):需要進行嵌套循環計算。兩個關聯表join,關聯字段均未建立索引,就會出現這種情況。常見的優化方案是,在關聯字段上添加索引,避免每次嵌套循環計算。(性能較差,需要優化)
Join算法詳解
摘自 https://blog.csdn.net/u010841296/article/details/89790399
Mysql利用Nested-Loop Join 的算法思想去優化join,Nested-Loop Join翻譯成中文則是“嵌套循環連接”。
舉個例子:
select * from t1 inner join t2 on t1.id=t2.tid
(1)t1稱為外層表,也可稱為驅動表。
(2)t2稱為內層表,也可稱為被驅動表。
偽代碼表示如下:
List<Row> result = new ArrayList<>(); for(Row r1 in List<Row> t1){ for(Row r2 in List<Row> t2){ if(r1.id = r2.tid){ result.add(r1.join(r2)); } } }
在Mysql的實現中,Nested-Loop Join有3種實現的算法:
- Simple Nested-Loop Join:SNLJ,簡單嵌套循環連接
- Index Nested-Loop Join:INLJ,索引嵌套循環連接
- Block Nested-Loop Join:BNLJ,緩存塊嵌套循環連接
在選擇Join算法時,會有優先級,理論上會優先判斷能否使用INLJ、BNLJ:
Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join
Simple Nested-Loop
簡單嵌套循環連接實際上就是簡單粗暴的嵌套循環,如果table1有1萬條數據,table2有1萬條數據,那么數據比較的次數=1萬 * 1萬 =1億次(笛卡爾集),這種查詢效率會非常慢。
所以Mysql繼續優化,衍生出Index Nested-LoopJoin、Block Nested-Loop Join兩種NLJ算法。在執行join查詢時mysql會根據情況選擇兩種之一進行join查詢。
Index Nested-LoopJoin(減少內層表數據的匹配次數)
索引嵌套循環連接是基於索引進行連接的算法。索引是基於內層表的,通過外層表匹配條件直接與內層表索引進行匹配,避免和內層表的每條記錄進行比較, 從而利用索引的查詢減少了對內層表的匹配次數,優勢極大的提升了 join的性能:
原來的匹配次數 = 外層表行數 * 內層表行數
優化后的匹配次數 = 外層表的行數 * 內層表索引的高度
只有內層表join的列有索引時,才能用到Index Nested-LoopJoin進行連接。
由於用到索引,如果索引是輔助索引而且返回的數據還包括內層表的其他數據,則會回內層表查詢數據,多了一些IO操作。
Block Nested-Loop Join(減少內層表數據的循環次數)
緩存塊嵌套循環連接通過一次性緩存多條數據,把參與查詢的列緩存到Join Buffer 里,然后拿join buffer里的數據批量與內層表的數據進行匹配,從而減少了內層循環的次數(遍歷一次內層表就可以批量匹配一次Join Buffer里面的外層表數據)。
當不使用Index Nested-Loop Join的時候,默認使用Block Nested-Loop Join。
什么是Join Buffer?
- Join Buffer會緩存所有參與查詢的列而不是只有Join的列。
- 可以通過調整join_buffer_size緩存大小
- join_buffer_size的默認值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G,而之后的版本才能在64位操作系統下申請大於4G的Join Buffer空間。
- 使用Block Nested-Loop Join算法需要開啟優化器管理配置的optimizer_switch的設置block_nested_loop為on,默認為開啟。
如何優化Join速度
用小結果集驅動大結果集,減少外層循環的數據量,從而減少內層循環次數:如果小結果集和大結果集連接的列都是索引列,mysql在內連接時也會選擇用小結果集驅動大結果集,因為索引查詢的成本是比較固定的,這時候外層的循環越少,join的速度便越快。
為匹配的條件增加索引:爭取使用INLJ,減少內層表的循環次數.
增大join buffer size的大小:當使用BNLJ時,一次緩存的數據越多,那么內層表循環的次數就越少.
減少不必要的字段查詢:
- 當用到BNLJ時,字段越少,join buffer 所緩存的數據就越多,內層表的循環次數就越少;
- 當用到INLJ時,如果可以不回表查詢,即利用到覆蓋索引,則可能可以提示速度。(未經驗證,只是一個推論)
最終解決方案
有一說一,由於業務關系,我這次問題的解決方案是不用join。但我在實踐中發現,用join代替left join也可以使速度變快,如果我以后研究出了個所以然,會在這里繼續更新的。