MySQL 性能调优专题二(Explain执行计划使用详解)

栏目: 数据库 · 发布时间: 5年前

内容简介:id = 主键、 name = 学生姓名、 update_time = 更新时间id = 主键、 name = 课程名称(普通索引)id = 主键、 student_id = 学生表主键、 course_id = 课程表主键、 remark = 备注
  • EXPLAIN 分析你的 查询语句 或是 结构的性能瓶颈

  • 使用EXPLAIN 关键字可以模拟优化器执行 SQL 语句,从而知道 MySQL 是如何处理你的 SQL 语句的。

  • 在日常工作中,我们有时会碰到执行较慢的 SQL,此时我们可以使用 EXPLAIN 关联字来执行 SQL,可以查看到 SQL 语句有没有用到索引,是不是全表扫描等等,这些都可以通过 EXPLIN 命令来查看,我们可以通过查看到的信息进行进一步的优化。

  • 在SELECT语句之前增加EXPLAIN关键字,MySQL 会在查询上设置一个标识,执行查询时, 会返回执行计划的信息,而不是执行这条 SQL

数据准备

  • 学生表

id = 主键、 name = 学生姓名、 update_time = 更新时间

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入数据
INSERT INTO `student` (`id`, `name`, `update_time`) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18');
复制代码
  • 课程表

id = 主键、 name = 课程名称(普通索引)

DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入数据
INSERT INTO `course` (`id`, `name`) VALUES (1,'Java'), (2,'Python'), (3,'JS');
复制代码
  • 学生和课程关系表

id = 主键、 student_id = 学生表主键、 course_id = 课程表主键、 remark = 备注

其中 student_id 和 course_id 为 复合索引

DROP TABLE IF EXISTS `student_course`;
CREATE TABLE `student_course` (
  `id` int(11) NOT NULL,
  `student_id` int(11) NOT NULL,
  `course_id` int(11) NOT NULL,
  `remark` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_student_course_id` (`student_id`,`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入数据
INSERT INTO `student_course` (`id`, `student_id`, `course_id`) VALUES (1,1,1),(2,1,2),(3,2,1); 
复制代码

EXPLAIN 输出结果集

到现在为止,需要准备的表已经完成了,我们先简单使用下 EXPLAIN 看下都有哪些东西。

explain select * from actor; 
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

执行上面这条语句,会得到这么一个结果集,稍后我们详细介绍每个字段的含义。

在使用EXPLAIN时,在查询中的每个表都会输出一行记录(一条SQL中有几个 SElECT 就会有几条记录)。

EXPLAIN 有两个变种:

  • explain extended: 会在 explain 的基础上额外提供一些查询优化的信息。在其后通过 show warnings; 命令可以得到优化后的查询语句,可以得到优化后的查询语句,从而看出优化器优化了什么。(这个可以优化一些简单的SQL, 稍微复杂一点的还是自己动手吧 ヾ(=・ω・=)o )
explain extended select * from student WHERE id = 1; 
SHOW WARNINGS;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • explain partitions: 相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。

EXPLAIN 中的列

接下来我们将展示 EXPLAIN 中每个列的信息。

id 列

id 列的编号是 SELECT 的序列号,有几个 SELECT 就有几个 id,并且 id 的顺序是按 SELECT 出现的顺序增长的。 id 列越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行

  • 简单子查询
EXPLAIN SELECT (SELECT 1 FROM student LIMIT 1) FROM course;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • FROM 子句中的子查询
EXPLAIN SELECT id FROM (SELECT id FROM student) AS stu;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

注:我使用的 5.7 的版本,在执行后,只是一条简单的查询,返回结果里并没有用到临时表。

我在使用 5.6 版本测试的时候,执行该语句,得到的结果集中,出现了临时表。

在5.7之前的版本,会创建一个临时表,而外部的 SELECT 查询时用到了这个临时表,例如下图:

MySQL 性能调优专题二(Explain执行计划使用详解)
id 为 2 的 select_type 为 DERIVER

,id 为 1 的 在查询时 table 为 说明用到了临时表。

查看了官方文档,官方文档这样写道: 使用了合并或实现来优化派生表和视图引用 (原谅我个英语渣渣,直接谷歌翻译了( ̄. ̄),英文好的大佬可以直接看官网原文)。

优化器可以使用两种策略(也适用于视图引用)处理派生表引用:

  • 将派生表合并到外部查询块中

  • 将派生表实现为内部临时表

例1:
SELECT * FROM (SELECT * FROM t1) AS derived_t1;

通过合并派生表 derived_t1,该查询的执行类似于:

SELECT * FROM t1;

例2:

SELECT *
  FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2 ON t1.f2=derived_t2.f1
  WHERE t1.f1 > 0;
  
通过合并派生表 derived_t2,该查询的执行类似于:

SELECT t1.*, t2.f1
  FROM t1 JOIN t2 ON t1.f2=t2.f1
  WHERE t1.f1 > 0;
复制代码

简而言之就是:优化器以相同的方式处理派生表和视图引用:它尽可能避免不必要的实现,这样可以将条件从外部查询推送到派生表,并生成更高效的执行计划。

  • union查询
EXPLAIN SELECT 1 UNION ALL SELECT 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

注:和 FROM 子句中的子查询 一样,5.7之后的版本进行了优化。

select_type列

select_type 表示对应行是简单还是复杂的查询,如果是复杂的查询,又是上述三种复杂查询中的哪一种。

  • simple

简单查询

EXPLAIN SELECT * FROM student WHERE id = 2;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • primary

复杂查询中最外层的 select

  • subquery

包含在 select 中的子查询(不在 from 子句中)

  • derived

MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义)

用这个例子来了解 primary、subquery 类型

EXPLAIN SELECT (SELECT 1 FROM student WHERE id = 1) FROM (SELECT * FROM course WHERE id = 1) c;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • union

在 union 中的第二个和随后的 select

  • union result

从 union 临时表检索结果的 select

table列

这一列表示 EXPLAIN 的一行正在访问哪个表。

type列(重要)

这一列表示关联类型或访问类型,即 MySQL 决定如何查找表中的行,查找数据行记录的大概范围。

依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL,一般来说,得保证查询达到 range 级别,最好达到 ref

  • NULL

MySQL 能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。

例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表。

EXPLAIN SELECT MIN(id) FROM student;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • const、system

MySQL 能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。

  • 用于 primary keyunique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。

  • system 是 const 的特例,表里只有一条元组匹配时为 system

EXPLAIN extended SELECT * FROM (SELECT * FROM course WHERE id = 1) tmp;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
show warnings; 
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • eq_ref

primary key或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。

EXPLAIN SELECT * FROM student_course LEFT JOIN course ON student_course.course_id = course.id;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • ref

相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。

  • 简单 SELECT 查询 ,name 是普通索引(非唯一索引)
EXPLAIN SELECT * FROM course WHERE name = "Java";
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • 关联表查询 ,idx_student_course_id 是 student_id 和 course_id 的联合索引,这里使用到了 student_course 的左边前缀 student_id 部分。
EXPLAIN SELECT student_id FROM student LEFT JOIN student_course ON student.id = student_course.student_id;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • range

范围扫描通常出现在 IN()、BETWEEN、>、<、>= 等操作中。使用一个索引来检索给定范围的行。

EXPLAIN SELECT * FROM student WHERE id > 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • index

扫描全表索引,这通常比ALL快一些。(index是从索引中读取的,而all是从硬盘中读取)

EXPLAIN SELECT * FROM course;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • ALL

即全表扫描,意味着 MySQL 需要从头到尾去查找所需要的行。通常情况下这需要增加索引来进行优化了。

EXPLAIN SELECT * FROM student;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

possible_keys列

这一列显示查询可能使用哪些索引来查找。

  • 使用 EXPLAIN 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,MySQL 认为索引对此查询帮助不大,选择了全表查询。

  • 如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 WHERE 子句看是否可以创造一个适当的索引来提高查询性能,然后用 EXPLAIN 查看效果。

key列

这一列显示 MySQL 实际采用哪个索引来优化对该表的访问。

  • 如果没有使用索引,则该列是 NULL。

  • 如果想强制 MySQL 使用或忽视 possible_keys 列中的索引,在查询中使用 force index、ignore index。

key_len列

这一列显示了 MySQL 在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。

key_len 计算规则如下:

  • 字符串

    类型 长度
    char(n) n字节长度
    varchar(n) 2字节存储字符串长度,如果是utf-8,则长度 3n + 2
  • 数值类型

    类型 长度
    tinyint 1字节
    tinyint 2字节
    int 4字节
    bigint 8字节
  • 时间类型

    类型 长度
    date 3字节
    timestamp 4字节
    timestamp 8字节
  • 如果字段允许为 NULL,需要1字节记录是否为 NULL

索引最大长度是768字节,当字符串过长时,MySQL 会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

ref列

这一列显示了在 key 列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:student.id)

rows列

这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。

Extra列(重要)

这一列展示的是额外信息。常见的重要值如下:

  • Using index

查询的列被索引覆盖,并且 WHERE 筛选条件是索引的前导列,是性能高的表现。一般是使用了覆盖索引(索引包含了所有查询的字段)。对于 InnoDB 来说,如果是辅助索引性能会有不少提高。

EXPLAIN SELECT student_id FROM student_course WHERE student_id = 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • Using where

查询的列未被索引覆盖, WHERE 筛选条件非索引的前导列

EXPLAIN SELECT * FROM student WHERE NAME = 'Python';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • Using where Using index

查询的列被索引覆盖,并且 WHERE 筛选条件是索引列之一但是不是索引的前导列,意味着无法直接通过索引查找来查询到符合条件的数据

EXPLAIN SELECT course_id FROM student_course WHERE course_id = 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • NULL

查询的列未被索引覆盖,并且 WHERE 筛选条件是索引的前导列,意味着用到了索引,但是部分字段未被索引覆盖,必须通过 “回表” 来实现,不是纯粹地用到了索引,也不是完全没用到索引

EXPLAIN SELECT * FROM student_course WHERE student_id = 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • Using index condition

与 Using where 类似,查询的列不完全被索引覆盖, WHERE 条件中是一个前导列的范围

EXPLAIN SELECT * FROM student_course WHERE student_id > 1;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • Using temporary

MySQL 需要创建一张临时表来处理查询。出现这种情况一般是 要进行优化 的,首先是想到用 索引 来优化。

  • student.name没有索引,此时创建了张临时表来 DISTINCT
EXPLAIN SELECT DISTINCT name FROM student; 
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • course.name建立了idx_name索引,此时查询时 extra 是 Using index,没有用临时表 。
EXPLAIN SELECT DISTINCT name FROM course;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • Using filesort

MySQL 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。此时 MySQL 会根据联接类型浏览所有符合条件的记录,并保存 排序 关键字和行指针,然后排序关键字并按顺序检索行信息。这种情况下也是要考虑使用索引来优化的。

  • student.name 未创建索引,会浏览 student 整个表,保存排序关键字 name 和对应的 id,然后排序 name 并检索行记录
EXPLAIN SELECT * FROM student ORDER BY name;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • course.name 建立了idx_name索引,此时查询时 extra 是 Using index。
EXPLAIN SELECT * FROM course ORDER BY name;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • filtered列

filtered 是一个半分比的值, rows * filtered / 100 可以估算出将要和 EXPLAIN 中前一个表进行连接的行数(前一个表指 EXPLAIN 中的 id 值比当前表 id 值小的表)。

趁热打铁来波实践

准备表和数据

DROP TABLE IF EXISTS `employees`;
CREATE TABLE `employees` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
  `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
  `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
  PRIMARY KEY (`id`),
  KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表';

INSERT INTO employees(name,age,position,hire_time) VALUES('小明',22,'JAVA',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('李雷', 23,'Python',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('韩梅梅',23,'JS',NOW());
复制代码

如上, nameageposition复合索引

全值匹配

EXPLAIN SELECT * FROM employees WHERE NAME= '小明';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
EXPLAIN SELECT * FROM employees WHERE NAME= '小明' AND age = 22 AND POSITION ='JAVA';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

如上, 三条 SQL 都用到了索引

最佳左前缀法则

EXPLAIN SELECT * FROM employees WHERE NAME = '李磊';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

如图,可以看到 用到了索引

EXPLAIN SELECT * FROM employees WHERE POSITION = 'Python';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

如图,可以看到 没有用索引

EXPLAIN SELECT * FROM employees WHERE age = 23 AND POSITION ='Python';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

如图,可以看到 也没有用索引

总结: 如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

不在索引列上做任何操作

例如 计算、函数、(自动or手动)类型转换等,会导致索引失效而转向全表扫描

EXPLAIN SELECT * FROM employees WHERE left(NAME,1) = '韩';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

如图,可以看到 没有用索引

不能使用索引中范围条件右边的列

EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22 AND position ='JAVA';
通过上边实践,知道该语句用到了索引
复制代码
EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age > 22 AND position ='JAVA';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

我们可以计算下 key_len 长度,可以得知 索引只用到了 nameage , position 没用用到索引,所以 在复合索引中使用了范围条件右边的列索引会失效。

使用覆盖索引

只访问索引的查询(索引列包含查询列),减少 SELECT * 语句

EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22 AND POSITION ='Java';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
EXPLAIN SELECT NAME, age, POSITION FROM employees WHERE NAME= '小明' AND age = 22 AND POSITION ='Java';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

可以看到, 从 NULL 变成了 Using index

使用不等于(!=或者<>)时索引失效

EXPLAIN SELECT * FROM employees WHERE NAME != '小明'
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

IS NULL,IS NOT NULL 也无法使用索引

EXPLAIN SELECT * FROM employees WHERE name IS NOT NULL
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

LIKE 以通配符开头索引会失效

EXPLAIN SELECT * FROM employees WHERE name LIKE '%李'
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • 通配符放结尾索引不会失效
EXPLAIN SELECT * FROM employees WHERE name LIKE '李%'
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)
  • 或者使用覆盖索引,查询字段必须是建立覆盖索引字段
EXPLAIN SELECT NAME, age, position FROM employees WHERE name LIKE '%李'
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

注:当覆盖索引指向的字段是 varchar(380) 及 380 以上的字段时,覆盖索引会失效!

字符串不加单引号索引失效

EXPLAIN SELECT * FROM employees WHERE NAME = 1000;
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

少用or,用它连接很多情况下索引会失效

EXPLAIN SELECT * FROM employees WHERE name = '李磊' OR NAME = '韩梅梅';
复制代码
MySQL 性能调优专题二(Explain执行计划使用详解)

总结

假设 a、b、c 为复合索引

WHERE 语句 索引使用情况
WHERE a = '小明' 使用到 a
WHERE a = '小明' AND b = '李磊' 使用到 a 、b
WHERE a = '小明' AND b = '李磊' AND c = '韩梅梅' 使用到 a、b、c
WHERE b = '李磊' 或者 WHERE b = '李磊' AND c = '韩梅梅' 或者 WHERE c = '韩梅梅' 没有用到
WHERE a = '小明' AND c = '韩梅梅' a 用到了,c 没有用到,因为 b 中间断了
WHERE a = '小明' AND b > '李磊' AND c = '韩梅梅' a、b 用到了,c 不能用在范围后
WHERE a = '小明' AND b = '李磊%' AND c = '韩梅梅' 使用到 a、b、c
WHERE a = '小明' AND b = '%李磊' AND c = '韩梅梅' 只用到 a
WHERE a = '小明' AND b = '%李磊%' AND c = '韩梅梅' 只用到 a
WHERE a = '小明' AND b = '李%磊%' AND c = '韩梅梅' 使用到 a、b、c

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Responsive Web Design

Responsive Web Design

Ethan Marcotte / Happy Cog / 2011-6 / USD 18.00

From mobile browsers to netbooks and tablets, users are visiting your sites from an increasing array of devices and browsers. Are your designs ready? Learn how to think beyond the desktop and craft be......一起来看看 《Responsive Web Design》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具