一、explain工具介绍
-
使用explain关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。
-
通过explain执行SQL语句还能查看优化器优化之后的SQL,紧随其后通过
SHOW WARNINGS;
命令可以得到优化后的查询语句,从而看出优化器优化了什么# 例如 EXPLAIN SELECT * FROM role; SHOW WARNINGS;
二、添加示例表和数据用于后续演示
这里会使用角色加菜单关系三张表来做演示。
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`del_flag` tinyint(4) NULL DEFAULT 0 COMMENT '删除标识 0:已删除 1:未删除 默认0',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT = '菜单表';
INSERT INTO `menu` VALUES (1, '系统管理', '管理系统用户角色菜单相关', 0, '2023-09-28 10:45:41');
INSERT INTO `menu` VALUES (2, '商品管理', '管理系统商品', 0, '2023-09-28 10:46:04');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(50) DEFAULT NULL COMMENT '角色名称',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`del_flag` tinyint(4) NULL DEFAULT 0 COMMENT '删除标识 0:已删除 1:未删除 默认0',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_roleName` (`role_name`)
) ENGINE = InnoDB COMMENT = '角色表';
INSERT INTO `role` VALUES (1, '超级管理员', '最大角色', 0, '2023-09-28 10:43:31');
INSERT INTO `role` VALUES (2, '普通人员', '小卡拉米', 0, '2023-09-28 10:45:14');
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色菜单关联ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_roleId_menuId` (`role_id`,`menu_id`)
) ENGINE = InnoDB COMMENT = '角色菜单关联表';
INSERT INTO `role_menu` VALUES (1, 1, 1);
INSERT INTO `role_menu` VALUES (2, 1, 2);
INSERT INTO `role_menu` VALUES (3, 2, 2);
三、explain中的列
explain中有12个列分别代表不同指标,这里会举例介绍。
3.1、id列
id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的,id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
3.2、select_type列
select_type 表示对应行是简单还是复杂的查询。
- 1、simple:简单查询,查询不包含子查询和union
EXPLAIN SELECT * FROM role WHERE id = 1;
- 2、primary:复杂查询中最外层的 select
- 3、subquery:包含在 select 中的子查询(不在 from 子句中)
EXPLAIN SELECT t1.id,(SELECT menu_name FROM menu WHERE id=t1.menu_id ) AS menuName FROM role_menu t1
- 4、union:在 union 中的第二个和随后的 select
EXPLAIN SELECT * FROM role WHERE id=1 UNION SELECT * FROM role WHERE id=2;
3.3、table列
这一列表示 explain 的一行正在访问哪个表,如果表设置了别名会显示表的别名。
3.4、partitions列
指分区信息。数据库优化有分库、分表、分区;这里的分区值表的分区信息。
3.5、type列
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
一般来说,得保证查询达到range级别,最好达到ref
NULL
mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表。
EXPLAIN SELECT MIN(id) FROM role;
system
system是const的特例,表里只有一条元组匹配时为system,可以看SHOW WARNINGS;
的结果。
EXPLAIN SELECT * FROM (SELECT '666' AS num FROM dual) t1;
SHOW WARNINGS;
const
mysql能对查询的某部分进行优化并将其转化成一个常量(可以看SHOW WARNINGS;
的结果)。用于primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。
EXPLAIN SELECT * FROM role WHERE id=1;
SHOW WARNINGS;
eq_ref
primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。
EXPLAIN
SELECT * FROM role t1 INNER JOIN role_menu t2 ON t1.id=t2.id where t1.id;
ref
相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。
EXPLAIN SELECT * FROM role where role_name = '超级管理员';
range
范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。
EXPLAIN SELECT * FROM role where id < 3;
index
扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些。
EXPLAIN SELECT role_name FROM role;
ALL
即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。
EXPLAIN SELECT * FROM role WHERE del_flag = 1;
3.6、possible_keys列
这一列显示查询可能使用哪些索引来查找。
- explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引
对此查询帮助不大,选择了全表查询。 - 如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提
高查询性能,然后用 explain 查看效果。
3.7、key列
这一列显示mysql实际采用哪个索引来优化对该表的访问。
- 如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force
index、ignore index。# 强制使用idx_roleName索引 # 需要注意的就算我们指定了需要使用的索引,但是MySQL优化器觉得全表扫描更加高效或者我们指定的索引查询条件无法使用到,还是可能会忽略我们指定的索引。 EXPLAIN SELECT * FROM role FORCE INDEX(idx_roleName) WHERE role_name = '超级管理员'; # 忽略使用idx_roleName索引 EXPLAIN SELECT * FROM role IGNORE INDEX(idx_roleName) WHERE role_name = '超级管理员';
3.8、key_len列
这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。
举例来说,role_menu的联合索引 idx_roleId_menuId由 role_id 和 menu_id 两个bigint列组成,并且每个bigint是8字节。通
过结果中的key_len=8可推断出查询使用了第一个列:role_id 列来执行索引查找。
EXPLAIN SELECT * FROM role_menu WHERE role_id = 1;
- key_len计算规则如下:
- 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节,如果是utf-8mb4一个汉字占4个字节
- char(n):如果存汉字长度就是 3n 字节
- varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串
- 数值类型
- tinyint:1字节
- smallint:2字节
- int:4字节
- bigint:8字节
- 时间类型
- date:3字节
- timestamp:4字节
- datetime:8字节 (5.6.4开始,存储开销从8字节降到了5字节)
- 如果字段允许为 NULL,需要1字节记录是否为 NULL
- 索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。
- 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节,如果是utf-8mb4一个汉字占4个字节
3.9、ref列
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:role.id)
3.10、rows列
这一列是MySQL估计要读取并检测的行数,注意这个不是结果集里的行数,这个只是预估值和实际值可能有偏差。
3.11、filtered列
filtered指标表示扫描行数和条件过滤结果集的百分比,比如我们这边查询 role_name = ‘超级管理员’,全表扫描总扫描数据集2条过滤出1条得到比值50%。
EXPLAIN SELECT * FROM role WHERE role_name = '超级管理员';
3.12、Extra列
这一列展示的是额外信息。常见的重要值如下:
Using index
使用覆盖索引,mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值
EXPLAIN SELECT role_name FROM role WHERE role_name = '超级管理员';
Using where
使用 where 语句来处理结果,并且查询的列未全部被索引覆盖
EXPLAIN SELECT * FROM role WHERE role_name = "超级管理员" AND remark = "最大角色";
Using index condition
尝试只使用索引来获取数据,即能用索引就用;
EXPLAIN SELECT * FROM role WHERE role_name > '超级管理员';
Using temporary
用临时表存储中间结果,常用于DISTINCT、GROUP BY、ORDER BY 等操作。
EXPLAIN SELECT DISTINCT remark FROM role;
Using filesort
将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。
EXPLAIN SELECT * FROM role ORDER BY create_time;
Select tables optimized away
使用某些聚合函数(比如 max、min)来访问存在索引的某个字段。
EXPLAIN SELECT MIN(id) FROM role;