【MySQL】表字段字符集不同導致的索引失效問題

問題背景

在一次開發任務中,同事跟我說他開發的一個列表分頁查詢請求要耗時10多分鐘,查詢SQL是新建的一個表關聯了兩張主表,主表數據量較大。但正常情況下如果有索引的話,查詢也不會慢,這顯然是有問題的。

問題原因

通過排查,得知問題原因在於新建的表字符集用的是utf8mb4,而之前的兩張主表的字符集設置的是utf8,查詢進行表關聯,導致索引失效。

問題再現

我們可以通過新建兩張表,插入一些數據,查看sql執行計劃,再現問題。

  • 創建表及初始化數據:
-- 表1utf8字符集,並建立code和name列索引
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`code` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 表2 utf8mb4字符集,並建立code和name列索引
CREATE TABLE `t2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`code` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

INSERT INTO `t1` (`name`,`code`) VALUES ('zzz','00006'),('xxx','00002'),('aaa','000003'),('sss','000004'),('ddd','000005');

INSERT INTO `t2` (`name`,`code`) VALUES ('zzz','00001'),('xxx','00002'),('aaa','000003'),('sss','000004'),('ddd','000005');
  • 聯表查詢SQL及對應的執行計劃結果:
-- t1全表掃描
desc select * from t1 left join t2 on t1.code = t2.code where t2.name = 'ddd';

在這裏插入圖片描述

-- t1全表掃描
desc select * from t2 left join t1 on t1.code = t2.code where t2.name = 'ddd';

在這裏插入圖片描述

-- 兩個表索引都有效
desc select * from t2 left join t1 on t1.code = t2.code where t1.name = 'qqq';

在這裏插入圖片描述

-- 兩個表索引都有效
desc select * from t1 left join t2 on t1.code = t2.code where t1.name = 'qqq';

在這裏插入圖片描述

  • 索引失效原因:

我們可以使用以下語句,得到更加詳細的SQL執行計劃

(1) t2 left join t1

EXPLAIN EXTENDED select * from t2 left join t1 on t1.code = t2.code where t2.name = 'ddd';
SHOW WARNINGS;

結果如下:

/* select#1 */ select `demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code`,`demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code` from `demo`.`t2` left join `demo`.`t1` on((convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`)) where (`demo`.`t2`.`name` = 'ddd')

(2) t1 left join t2

EXPLAIN EXTENDED select * from t1 left join t2 on t1.code = t2.code where t2.name = 'ddd';
SHOW WARNINGS;

結果如下:

/* select#1 */ select `demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code`,`demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code` from `demo`.`t1` join `demo`.`t2` where ((`demo`.`t2`.`name` = 'ddd') and (convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`))

從上面可以看出,不論是t1 join t2,還是t2 join t1,在查詢過程中會對t1表進行了一次字符集的轉換(convert(demo.t1.code using utf8mb4) )。字符集轉換遵循由小到大的原則,因爲utf8mb4是utf8的超集,所以這裏把utf8轉換成utf8mb4。而實際上t1表中的索引是utf8格式的,所以會導致t1表全表掃描。

但如果我們以t1中的字段作爲查詢條件的話,兩個表的索引是可以都生效的:

EXPLAIN EXTENDED select * from t2 left join t1 on t1.code = t2.code where t1.`code` = '00001';
SHOW WARNINGS;

通過以下執行計劃結果,可以看出在轉換字符集之前,就先進行了查詢,所以t1表中的索引也使用到了:

/* select#1 */ select `demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code`,`demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code` from `demo`.`t2` join `demo`.`t1` where ((`demo`.`t1`.`code` = '00001') and (convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`))

解決方案

  1. 如上述實例,如果查詢字段在t1和t2表中都存在,可以選擇使用t1表中的字段進行查詢。
  2. 如果查詢字段必須通過t2表,則只能通過修改字符集處理,將關聯的表的字符集都修改成一樣的。

最後附上修改字符集sql:

alter table t1 convert to charset utf8mb4;

還要注意一點,alter table 改字符集的操作是阻塞寫的(用lock = node會報錯)所以業務高峯時不要操作。

問題總結

  1. 一個項目中的表和字段字符集最好統一,utf8還是utf8mb4按業務情況定好規則,不要隨意使用。
  2. 擅於利用執行計劃,優化SQL查詢的利器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章