問題背景
在一次開發任務中,同事跟我說他開發的一個列表分頁查詢請求要耗時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`))
解決方案
- 如上述實例,如果查詢字段在t1和t2表中都存在,可以選擇使用t1表中的字段進行查詢。
- 如果查詢字段必須通過t2表,則只能通過修改字符集處理,將關聯的表的字符集都修改成一樣的。
最後附上修改字符集sql:
alter table t1 convert to charset utf8mb4;
還要注意一點,alter table 改字符集的操作是阻塞寫的(用lock = node會報錯)所以業務高峯時不要操作。
問題總結
- 一個項目中的表和字段字符集最好統一,utf8還是utf8mb4按業務情況定好規則,不要隨意使用。
- 擅於利用執行計劃,優化SQL查詢的利器。