MySQL -- JOIN優化
CREATE TABLE t1(id INT PRIMARY KEY, a INT, b INT, INDEX(a)); CREATE TABLE t2 LIKE t1; DROP PROCEDURE idata; DELIMITER ;; CREATE PROCEDURE idata() BEGIN DECLARE i INT; SET i=1; WHILE (i <= 1000) DO INSERT INTO t1 VALUES (i,1001-i,i); SET i=i+1; END WHILE; SET i=1; WHILE (i <= 1000000) DO INSERT INTO t2 VALUES (i,i,i); SET i=i+1; END WHILE; END;; DELIMITER ; CALL idata();
Multi-Range Read
MRR的目的:儘量使用 順序讀盤
回表
SELECT * FROM t1 WHERE a>=1 AND a<=100;
如果隨著a遞增的順序進行查詢的話,id的值會變成隨機的,就會出現 隨機訪問 , 效能相對較差

MRR
- 根據索引a,定位到滿足條件的記錄,將id的值放入
read_rnd_buffer
中 - 將
read_rnd_buffer
中的id進行 遞增排序 - 排序後的id值,依次到主鍵索引中查詢
- 如果
read_rnd_buffer
滿,先執行完第2步和第3步,然後清空read_rnd_buffer
,繼續遍歷索引a
-- 預設值為256KB -- 8388608 Bytes = 8 MB mysql> SHOW VARIABLES LIKE '%read_rnd_buffer_size%'; +----------------------+---------+ | Variable_name| Value| +----------------------+---------+ | read_rnd_buffer_size | 8388608 | +----------------------+---------+ -- mrr_cost_based=on:現在的優化器基於消耗的考慮,更傾向於不使用MRR mysql> SHOW VARIABLES LIKE '%optimizer_switch%'\G; *************************** 1. row *************************** Variable_name: optimizer_switch Value: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=off -- 穩定啟動MRR優化 SET optimizer_switch='mrr_cost_based=off';
執行流程
EXPLAIN
mysql> SET optimizer_switch='mrr_cost_based=on'; Query OK, 0 rows affected (0.00 sec) -- 優化器沒有選擇MRR mysql> EXPLAIN SELECT * FROM t1 WHERE a>=1 AND a<=100; +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+ |1 | SIMPLE| t1| NULL| range | a| a| 5| NULL |100 |100.00 | Using index condition | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SET optimizer_switch='mrr_cost_based=off'; Query OK, 0 rows affected (0.00 sec) -- 優化器選擇了MRR mysql> EXPLAIN SELECT * FROM t1 WHERE a>=1 AND a<=100; +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------+ |1 | SIMPLE| t1| NULL| range | a| a| 5| NULL |100 |100.00 | Using index condition; Using MRR | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------+
小結
MRR提升效能的核心:能夠在索引a上做 範圍查詢 ,得到 足夠多的主鍵 ,完成 排序 後再回表,體現出 順序性 的優勢
NLJ優化
NLJ演算法
從驅動表t1,一行行地取出a的值,再到被驅動表t2去join,此時 沒有利用到MRR的優勢

BKA優化
Batched Key Access
,是MySQL 5.6引入的對 Index Nested-Loop Join
( NLJ
)的優化

-
BKA
優化的思路: 複用join_buffer
- 在
BNL
演算法中,利用了join_buffer
來暫存驅動表的資料,但在NLJ
裡面並沒有利用到join_buffer
- 在
join_buffer
中放入的資料為P1~P100,表示只會取 查詢所需要的欄位- 如果
join_buffer
放不下P1~P100,就會將這100行資料 分成多段 執行
- 如果
啟用
-- BKA演算法依賴於MRR SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
BNL優化
效能問題
- 使用
BNL
演算法,可能會 對被驅動表做多次掃描 ,如果被驅動表是一個 大的冷資料表 ,首先 IO壓力會增大 - Buffer Pool的LRU演算法
old
- 如果一個使用了BNL演算法的Join語句,多次掃描一個冷表
- 如果 冷表不大 ,能夠 完全放入old區
- 再次掃描冷表的時候,會把冷表的資料頁移到LRU連結串列頭部, 不屬於期望的晉升
- 如果 冷表很大 , 業務正常訪問的資料頁,可能沒有機會進入young區
- 一個正常訪問的資料頁,要進入young區,需要隔1S後再次被訪問
- 由於Join語句在迴圈讀磁碟和淘汰記憶體頁,進入old區的資料頁,很有可能在1S內被淘汰
- 正常業務訪問的資料頁也 一併被沖掉 ,影響正常業務的記憶體命中率
- 如果 冷表不大 ,能夠 完全放入old區
- 大表Join雖然對IO有影響,但在語句執行結束後,對IO的影響也就結束了
- 但 對Buffer Pool的影響是持續性的 ,需要依靠後續的查詢請求慢慢恢復記憶體命中率
- 為了減少這種影響,可以考慮適當地增大
join_buffer_size
,減少對被驅動表的掃描次數
- 小結
- 可能會多次掃描 被驅動表 ,佔用磁碟 IO資源
- 判斷Join條件需要執行$M*N$次對比,如果是大表會佔用非常多的 CPU資源
- 可能會導致Buffer Pool的 熱資料被淘汰 和 正常的業務資料無法成為熱資料 ,進而影響 記憶體命中率
- 如果優化器選擇了
BNL
演算法,就需要做優化- 給被驅動表 Join欄位 加索引,把
BNL
演算法轉換成BKA
演算法 - 臨時表
- 給被驅動表 Join欄位 加索引,把
不適合建索引
t2中需要參與Join的只有2000行,並且為一個 低頻語句 ,為此在t2.b上建索引是比較浪費的
SELECT * FROM t1 JOIN t2 ON (t1.b=t2.b) WHERE t2.b>=1 AND t2.b<=2000;
採用BNL
- 取出t1的所有欄位,存入
join_buffer
(無序陣列),完全放得下 - 掃描t2,取出每一行資料跟
join_buffer
中的資料進行對比- 如果不滿足
t1.b=t2.b
,則跳過 - 如果 滿足
t1.b=t2.b
, 再判斷是否滿足其它條件 ,如果滿足就作為結果集的一部分返回,否則跳過
- 如果不滿足
- 等值判斷的次數為1000*100W= 10億 ,計算量很大
-- 使用BNL演算法 mysql> EXPLAIN SELECT * FROM t1 JOIN t2 ON (t1.b=t2.b) WHERE t2.b>=1 AND t2.b<=2000; +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key| key_len | ref| rows| filtered | Extra| +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+ |1 | SIMPLE| t1| NULL| ALL| NULL| NULL | NULL| NULL |1000 |100.00 | Using where| |1 | SIMPLE| t2| NULL| ALL| NULL| NULL | NULL| NULL | 998414 |1.11 | Using where; Using join buffer (Block Nested Loop) | +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+ -- 執行耗時為75S,非常久! mysql> SELECT * FROM t1 JOIN t2 ON (t1.b=t2.b) WHERE t2.b>=1 AND t2.b<=2000; ... |999 |2 |999 |999 |999 |999 | | 1000 |1 | 1000 | 1000 | 1000 | 1000 | +------+------+------+------+------+------+ 1000 rows in set (1 min 15.29 sec) # Time: 2019-03-11T12:04:49.066846Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 75.288703Lock_time: 0.000174 Rows_sent: 1000Rows_examined: 1001000 SET timestamp=1552305889; SELECT * FROM t1 JOIN t2 ON (t1.b=t2.b) WHERE t2.b>=1 AND t2.b<=2000;
臨時表
思路
BKA
執行過程
CREATE TEMPORARY TABLE temp_t (id INT PRIMARY KEY, a INT, b INT, INDEX(b)) ENGINE=InnoDB; INSERT INTO temp_t SELECT * FROM t2 WHERE b>=1 AND b<=2000; # Time: 2019-03-11T12:20:01.810030Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.624821Lock_time: 0.002347 Rows_sent: 0Rows_examined: 1000000 SET timestamp=1552306801; INSERT INTO temp_t SELECT * FROM t2 WHERE b>=1 AND b<=2000; -- 採用NLJ演算法,如果batched_key_access=on,將採用BKA優化 mysql> EXPLAIN SELECT * FROM t1 JOIN temp_t ON (t1.b=temp_t.b); +----+-------------+--------+------------+------+---------------+------+---------+-----------+------+----------+-------------+ | id | select_type | table| partitions | type | possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+--------+------------+------+---------------+------+---------+-----------+------+----------+-------------+ |1 | SIMPLE| t1| NULL| ALL| NULL| NULL | NULL| NULL| 1000 |100.00 | Using where | |1 | SIMPLE| temp_t | NULL| ref| b| b| 5| test.t1.b |1 |100.00 | NULL| +----+-------------+--------+------------+------+---------------+------+---------+-----------+------+----------+-------------+ -- 執行耗時為20ms,提升很大 mysql> SELECT * FROM t1 JOIN temp_t ON (t1.b=temp_t.b); ... |999 |2 |999 |999 |999 |999 | | 1000 |1 | 1000 | 1000 | 1000 | 1000 | +------+------+------+------+------+------+ 1000 rows in set (0.02 sec) # Time: 2019-03-11T12:20:11.041259Z # User@Host: root[root] @ localhost []Id:8 # Query_time: 0.012139Lock_time: 0.000187 Rows_sent: 1000Rows_examined: 2000 SET timestamp=1552306811; SELECT * FROM t1 JOIN temp_t ON (t1.b=temp_t.b);
- 執行
INSERT
語句構造tmp_t表並插入資料的過程中,對t2做了 全表掃描 ,掃描行數為100W - JOIN語句先掃描t1,掃描行數為1000,在JOIN的比較過程中,做了1000次 帶索引的查詢
Hash Join
- 如果
join_buffer
維護的不是一個無序陣列,而是一個 雜湊表 ,那隻需要100W次雜湊查詢即可 - MySQL目前不支援
Hash Join
,業務端可以自己實現Hash Join
-
SELECT * FROM t1
- 取t1的全部1000行資料,在業務端存入一個hash結構(如
java.util.HashMap
)
- 取t1的全部1000行資料,在業務端存入一個hash結構(如
-
SELECT * FROM t2 WHERE b>=1 AND b<=2000
,獲取t2中滿足條件的2000行資料 - 把這2000行資料,一行行地到hash結構去匹配,將滿足匹配條件的行資料,作為結果集的一行
-
小結
-
BKA
是MySQL 內建支援 的, 推薦使用 -
BNL
演算法 效率低 ,建議都儘量換成BKA
演算法,優化的方向是 給被驅動表的關聯欄位加上索引 - 基於 臨時表 的改進方案,對於能夠 提前過濾出小資料的JOIN語句 來說,效果還是很明顯的
- MySQL目前還不支援
Hash Join
參考資料
《MySQL實戰45講》
轉載請註明出處:http://zhongmingmao.me/2019/03/11/mysql-join-opt/
訪問原文「MySQL -- JOIN優化」獲取最佳閱讀體驗並參與討論