1. 程式人生 > 其它 >MySQL Execution Plan--Block Nested Loop執行計劃異常

MySQL Execution Plan--Block Nested Loop執行計劃異常

問題描述

在排查某次業務慢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'
    

相關資料