MySQL Execution Plan--Block Nested Loop執行計劃異常
阿新 • • 發佈:2022-04-13
問題描述
在排查某次業務慢SQL時,發現SQL執行計劃異常,業務表結構為:
CREATE TABLE `xxxx_user_cancellation` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '註冊使用者id', `nickname` varchar(20) NOT NULL DEFAULT '' COMMENT '暱稱', `user_type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '使用者型別', `identify_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '實名認證ID', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='xxxx使用者登出表'; CREATE TABLE `xxxx_register_user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `nickname` varchar(20) NOT NULL DEFAULT '' COMMENT '暱稱', `user_type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '使用者型別 1.遊客 2.房東 3.遊客房東', `user_phone` varchar(16) NOT NULL DEFAULT '' COMMENT '註冊手機號', `identify_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '實名認證ID', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間', PRIMARY KEY (`id`), KEY `idx_identify_id` (`identify_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB AUTO_INCREMENT=11904740 DEFAULT CHARSET=utf8mb4 COMMENT='xxxx註冊使用者';
業務查詢SQL為:
SELECT *
FROM xxxx_register_user r
LEFT JOIN xxxx_user_cancellation uc
ON r.id = uc.user_id
ORDER BY r.id DESC
LIMIT 0, 20;
對應執行計劃為:
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: r partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 4213143 filtered: 100.00 Extra: Using temporary; Using filesort *************************** 2. row *************************** id: 1 select_type: SIMPLE table: uc partitions: NULL type: ALL possible_keys: idx_user_id key: NULL key_len: NULL ref: NULL rows: 1 filtered: 100.00 Extra: Using where; Using join buffer (Block Nested Loop) 2 rows in set, 1 warning (0.00 sec)
問題分析
表上xxxx_user_cancellation上存在索引idx_user_id(user_id),對應關聯列xxxx_register_user.id的列型別和列編碼都相同,理論上完全可以使用該索引,但由於表xxxx_user_cancellation上僅有1條記錄,MySQL查詢優化器評估通過JOIN BUFER使用Block Nested Loop演算法"能有效提升兩表關聯的效能",Block Nested Loop演算法需要"buffer"外表一部分資料後再與內表進行JOIN操作:
For each tuple r in R do -- 掃描外表R store used columns as p from R in Join Buffer -- 將部分或者全部R的記錄儲存到Join Buffer中,記為p For each tuple s in S do -- 掃描內表S If p and s satisfy the join condition -- p與s滿足join條件 Then output the tuple -- 返回為結果集
由於使用Block Nested Loop演算法,導致查詢被轉換為:
SELECT * FROM (
SELECT *
FROM xxxx_register_user r
LEFT JOIN xxxx_user_cancellation uc
ON r.id = uc.user_id
) AS T1
ORDER BY T1.id DESC
LIMIT 0, 20;
由於外表資料量量較大(421萬行),對外表的全表掃描導致整個SQL執行耗時極高。
在當前查詢語句中使用LEFT JOIN + LIMIT 0, 20
語句,理論上外表最多僅需要掃描20行記錄即可返回,虛擬碼為:
def get_left_join_result():
join_rows = list()
for register_user_row in xxxx_register_user(order by id desc):
cancel_user_rows = get_rows(
"""
SELECT *
FROM xxxx_user_cancellation uc
WHERE uc.user_id = {}
""".format(register_user_row.id)
)
if len(cancel_user_rows)==0:
cancel_user_rows.append(None)
for cancel_user_row in cancel_user_rows:
join_row=(register_user_row,cancel_user_row)
join_rows.append(join_row)
if len(join_rows) > 20:
return join_rows
解決辦法
-
使用強制索引提示,如:
SELECT * FROM anban_register_user r LEFT JOIN anban_user_cancellation uc FORCE INDEX(idx_user_id) ON r.id = uc.user_id ORDER BY r.id DESC LIMIT 0, 20;
-
關閉Block Nested Loop演算法開關:
SET GLOBAL optimizer_switch='block_nested_loop=off'