“你一定又写了烂SQL了!”,“你怎么这样凭空污人清白……慢查询,慢查询不能算烂……慢查询!……程序猿的事,能算烂么?”
本文从SQL
执行效率方面略作研究,偏向基础性总结,但力求详实准确。如果有大佬误入此地,还请从容撤退,如果你真的愿意看,我也没什么意见。
EXPLAIN
关键字能够分析呈现Mysql
处理SQL
语句的诸多要素,进而分析SQL
语句的瓶颈所在。
在看下面内容之前,墙裂建议先看本文姊妹篇:图文并茂说透Mysql索引
EXPLAIN的使用非常简单,在SQL语句前加上EXPLAIN
即可,例如:
EXPLAIN SELECT * FROM students;
执行后,会出现下图所示的结果:
执行EXPLAIN
后出现上图所示结果表,读懂这个结果表才是关键,下面解释一下每个字段的含义(加粗行表示比较重要的项):
列 | 释义 |
---|---|
id | 列编号是 SELECT 的序列号,并且 id 的顺序是按 SELECT 出现的顺序增长的。id 列越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行。 |
select_type | SELECT关键字对应的查询类型 |
table | 表名、表别名或临时表的标识 |
partitions | 分区信息 |
type | 表示关联类型或访问类型,即MySQL决定如何查找表中的行 |
possible_keys | 可能用到的索引 |
key | 实际使用的索引 |
key_len | 实际使用的索引的长度 |
ref | 使用索引列等值查询时,与索引列等值匹配的对象信息 |
rows | 查询优化器估计要读取并检测的行数 |
Extra | 额外信息,如Using index、Using where等 |
1、select_type表示查询类型,包括简单查询、复杂查询、子查询等:
类型 | 释义 |
---|---|
SIMPLE | 简单的SELECT 查询,查询中不包含子查询或UNION |
PRIMARY | 查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY |
SUBQUERY | 在SELECT 或WHERE 中包含了子查询 |
DERIVED | 在FROM 中包含的子查询被标记为DERIVED ,MySQL 会递归执行这些子查询,把结果放在临时表里 |
UNION | 若第二个SELECT 出现在UNION 之后,则被标记为UNION ,若UNION 包含在FROM 子句的子查询中,外层SELECT 将被标记为DERIVED |
UNION RESULT | 从UNION 表获取结果的SELECT |
2、type表示关联类型或访问类型,即MySQL决定如何查找表中的行:
类型 | 释义 |
---|---|
system、const | const 表示查询使用了主键索引(primary key )或唯一索引,system 是表只有一行记录(等于系统表)时的type ,是 const 类型的特例 |
eq_ref | 在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref |
ref | 相比 eq_ref ,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行 |
ref_or_null | 对普通二级索引进行等值查询,该索引列也可以为NULL值时 |
index_merge | 使用不同的索引查询并将结果合并 |
range | 使用索引查询范围结果,通常出现在 in, between ,> ,<, >= 等操作中。 |
index | 查询语句对一个索引树进行了全量扫描 |
ALL | 全表扫描,MySQL会遍历所有行去查找结果,这种类型是效率最差的类型,必须进行索引优化 |
3、Extra表示额外信息:
类型 | 释义 |
---|---|
Using index | 查询语句只扫描一次索引树即获得了目标数据,效率很高,一般是通过索引列查询主键或查询与索引列建有联合索引的列 |
Using where Using index | 无法直接通过索引查找来查询到符合条件的数据,一般是使用索引前导列进行范围查询或通过索引的非前导列查询 |
Using index condition | 查询列的某一部分无法直接使用索引,一般是WHERE 条件列是索引前导列且是范围查询导致的 |
NULL | WHERE条件是索引前导列,但查询列至少有一个未与条件列在同一个索引树上,必须通过回表查询 |
Using where | WHERE 条件列上无索引(既没有单独索引,也没有联合索引),而与查询列无关 |
Using filesort | MySQL需要创建一张内部临时表来处理查询,通常在许多执行包括DISTINCT、GROUP BY、ORDER BY 等子句查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能会寻求建立内部临时表来执行查询 |
Using filesort | 当SQL中使用ORDER BY 关键字的时候,如果待排序的内容不能由所使用的索引直接完成排序的话,那么mysql有可能就要进行文件排序 |
Index merges | 对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union),一般出现AND和OR查询 |
下面,我会构造测试表与数据,详细演示每一个案例,请做好战斗准备!感兴趣的同学,可以自己建表动手测试一下,毕竟纸上得来终觉浅,绝知此事要躬行。演示数据在文末。
id
列是 SELECT
的序列号,其顺序是按 SELECT
出现顺序增长的。id
列越大执行优先级越高,id
相同则从上往下执行,id
为 NULL
最后执行。
MySQL将 SELECT
查询分为简单查询 SIMPLE
和复杂查询 PRIMARY
。复杂查询包括:简单子查询、派生表( FROM 语句中的子查询)、UNION 和 UNION ALL 查询。
1、SUBQUERY
在SELECT
或WHERE
中包含了子查询,例如:
EXPLAIN SELECT (SELECT 1 FROM student LIMIT 1) FROM student;
WHERE
条件中含子查询,select_type也会出现PRIMARY
与SUBQUERY
:
EXPLAIN SELECT id FROM student WHERE id = (SELECT s_id id FROM class_student WHERE c_id=3)
2、DERIVED
FROM
中包含的子查询被标记为DERIVED
,最外层查询被标记为PRIMARY
,如下面这个SQL:
EXPLAIN SELECT * FROM (SELECT * FROM student GROUP BY id) AS temp;
UNION
和UNION ALL
是对两个SQL结果进行纵向合并,即列数不变,行数增
加,前者对合并结果去重,后者不去重。
因此,UNION 会将合并结果放在一个匿名临时表中进而做去重操作,临时表不在 SQL 中出现,临时表名为
EXPLAIN SELECT * FROM student WHERE id =1 UNION SELECT * FROM student
UNION ALL 无需为合并结果去重,仅是将多个查询结果集中的记录合并成一个,所以不会使用到临时表,故没有 id 为 NULL 记录:
EXPLAIN SELECT * FROM student WHERE id =1 UNION ALL SELECT * FROM student
另外,我们通过EXPLAIN
可以看出,查询优化器往往会将SQL进行重写以达到优化效果,例如将子查询优化为连接查询:
EXPLAIN SELECT * FROM student WHERE id IN(SELECT s_id FROM class_student)
上面的子查询并没有出现预期的SUBQUERY
型select_type
:
table
列表示 EXPLAIN
的单独行的唯一标识符。这个值可能是表名、表的别名或者一个未查询产生临时表的标识符,如派生表、子查询或集合。当 FROM
子句中有子查询时,如果优化器采用的物化方式,table
列是
格式,表示当前查询依赖 id=N
的查询,于是先执行 id=N
的查询。
当使用 UNION
查询时,UNION RESULT
的 table
列的值为
,1和2表示参与 UNION
的 SELECT
的行 id
。
type 表示关联类型或访问类型,即MySQL决定如何查找表中的行,从最好到最差依次排列:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
1、system、const
system
是表只有一行记录(等于系统表)时的type
,是 const
类型的特例,平时不会出现。const
表示查询使用了主键索引(primary key
)或唯一索引(unique
),因为这两种索引具有唯一性,结果必然只匹配到一行数据,所以查询速度很快。
EXPLAIN SELECT * FROM student where id = 1
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref
。这可能是在 const
之外最好的联接类型了。
EXPLAIN SELECT * FROM class_student JOIN student ON class_student.s_id=student.id
这意味着WHERE
条件是索引前导列,且该索引是普通索引。如下SQL,name上建有普通索引:
EXPLAIN SELECT * FROM student WHERE name = '赵日天';
关联表查询,被驱动表使用普通索引,type也会是ref:
EXPLAIN SELECT s_id FROM student LEFT JOIN class_student ON student.id = class_student.s_id;
对普通二级索引进行等值查询,该索引列也可以为NULL值时,例如:
EXPLAIN SELECT * FROM student WHERE name = '李胜利' OR name IS NULL
顾名思义,使用不同的索引查询并将结果合并,例如:
EXPLAIN SELECT * FROM student WHERE student.name = '李胜利' OR id=3
使用索引查询范围结果,通常出现在 in, between ,> ,<, >= 等操作中。
EXPLAIN SELECT * FROM student WHERE id > 3
这种情况意味着查询语句对一个索引树进行了全量扫描,出现这种情况是因为:
例如,name和age建有联合索引,且name是索引前导列。先看第一种情况:
EXPLAIN SELECT id,name FROM student
EXPLAIN SELECT name FROM student WHERE age=17
8、ALL
全表扫描,MySQL会遍历所有行去查找结果,这种类型是效率最差的类型,很有必要进行索引优化。
出现这种情况,原因在于查询语句无法使用索引。假设name和age分别建有普通单列索引,create_time上无索引,下面两种情况会导致type为ALL:
EXPLAIN SELECT name FROM student WHERE create_time='2019'
EXPLAIN SELECT age,name FROM student
出现了ALL,SQL语句就很有优化的必要了,优化思路针对上面两种情形:要么对WHERE列加索引,要么保证查询列在同一个索引树上(比如建立联合索引)。
possible_keys
表示SQL可能使用哪些索引来查找。
EXPLAIN 执行计划结果可能出现 possible_keys 列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,MySQL 会认为索引对此查询帮助不大,选择了全表查询。
如果 possible_keys 列为 NULL,则没有相关的索引。在这种情况下,可以通过检查 WHERE 子句去分析下,看看是否可以创造一个适当的索引来提高查询性能,然后用 EXPLAIN 查看效果。
另外注意:不是这一列的值越多越好,使用索引过多,查询优化器计算时查询成本高,所以如果可能的话,尽量删除那些不用的索引。
key 列表示SQL实际采用了哪个索引来优化对该表的访问。如果没有使用索引,则该列是 NULL。如果想强制 MySQL使用或忽视 possible_keys 列中的索引,在查询中使用 force index、ignore index。
key_len
表示索引记录的最大长度。
key_len列计算规则如下:
索引最大长度是768字节,当字符串过长时,MySQL 会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。
例如:student表name字段类型为varchar(25),允许为NULL,那么下面SQL索引长度将为3*25+2+1=78
EXPLAIN SELECT id,name FROM student WHERE name='叶良辰'
ref
:显示了在 key 列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:student.id)。rows
:是查询优化器估计要读取并检测的行数,注意这个不是结果集里的行数。如果查询优化器使用全表扫描查询,rows 列代表预计的需要扫码的行数。如果查询优化器使用索引执行查询,rows 列代表预计扫描的索引记录行数。filtered
:对于单表来说意义不大,主要用于连接查询中。前文中也已提到 filtered 列,是一个百分比的值,对于连接查询来说,主要看驱动表的 filtered的值 ,通过 rows * filtered/100 计算可以估算出被驱动表还需要执行的查询次数。Extra 列提供了一些额外信息。这一列在 MySQL中提供的信息有几十个。
首先先解释几个概念:
class_student
表中,index_s_c_id
这个索引建立在s_id
和c_id
上,这个索引称之为复合索引,而s_id
为index_s_c_id
索引的前导列。下面看一下Extra中的几个重要的值:
1、Using index
释义: 这种情况意味着查询语句只扫描一次索引树即获得了目标数据,效率很高。
条件:
WHERE
条件列上创建有索引,且是索引前导列示例:
EXPLAIN SELECT id,name FROM student WHERE name='叶良辰'
例如上面的SQL中,id是主键,通过name的索引树可以获取id和name,不需要回表,符合索引覆盖。
当SQL变成下面的样子时,
EXPLAIN SELECT name,age FROM student WHERE name='叶良辰'
对应的索引是name和age建有单独索引:
查询结果可以看出未能达到索引覆盖,效率下降:
我们将name和age的索引改造为联合索引:
再次执行SQL,结果变成了下面这个样子:
以上探索给我们的启示是,对于频繁同时查询的多列,可以考虑建立联合索引来优化。
2、Using where Using index
释义: Mysql无法直接通过索引查找来查询到符合条件的数据。
条件:
WHERE
条件列不是索引前导列,查询列与条件列在同一个索引树上(查询列是主键或查询列与条件建有联合索引)WHERE
条件列是索引前导列但使用范围查询时,且查询列与条件列在同一个索引树上示例:
1)name和age有联合索引,以非前导列age为查询条件时
执行SQL:
EXPLAIN SELECT name FROM student WHERE age=17
2)调整索引,age为联合索引前导列,但使用age进行范围查询
执行SQL:
EXPLAIN SELECT id FROM student WHERE age>17
3、Using index condition
释义:这种情况意味着查询列的某一部分无法直接使用索引
条件:
WHERE
条件列是索引前导列且是范围查询WHERE
条件列是索引前导列且是后置模糊查询示例:
1)假如age是索引前导列,而create_time上无索引,以age为条件进行范围查询
EXPLAIN SELECT create_time FROM student WHERE age>17 AND age<20
2)假如name是索引前导列,而create_time上无索引,以name为条件进行后置模糊查询
EXPLAIN SELECT create_time FROM student WHERE name LIKE '叶%'
释义: 这种情况意味着WHERE条件是索引前导列,但查询列至少有一个未与条件列在同一个索引树上,必须通过回表查询。
示例: 例如name是索引前导列,而create_time上无索引
EXPLAIN SELECT create_time FROM student WHERE name ='叶良辰'
如果这种查询很频繁,可以通过将查询列与条件列建立联合索引来优化。
5、Using where
出现这种情况意味着, WHERE
条件列上无索引(既没有单独索引,也没有联合索引),而与查询列无关,例如:
EXPLAIN SELECT id FROM student WHERE create_time='2020-07-28 20:22:40'
可见,这种情况对应的type
为ALL
,也就是进行了全表扫描,效率堪忧。优化的方法很简单,给WHERE
条件列添加索引即可。
6、Using temporary
这意味着MySQL需要创建一张内部临时表来处理查询。临时表的建立与维护是需要付出很大成本的,因此执行计划中出现 Using temporary
并不是个好现象,需要考虑使用索引来进行优化。
通常在许多执行包括DISTINCT、GROUP BY、ORDER BY
等子句查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能会寻求建立内部临时表来执行查询,例如:EXPLAIN SELECT DISTINCT(create_time) FROM student
:
当我们对被索引覆盖的列进行去重查询时,结果会有很大不同:EXPLAIN SELECT DISTINCT(name) FROM student
。
7、Using filesort
当SQL中使用ORDER BY
关键字的时候,如果待排序的内容不能由所使用的索引直接完成排序的话,那么mysql有可能就要进行文件排序。例如:EXPLAIN SELECT name FROM student ORDER BY create_time
,create_time列未被索引覆盖,所以引发了Using filesort
。
不能说filesort一定会引发性能问题,但如果这种查询非常频繁,每次在Mysql中进行排序,还是有优化必要的。优化手段一是不使用ORDER
,而是在应用程序中完成排序,二是对需要排序的列添加索引,直接利用索引的排序。
例如:EXPLAIN SELECT id FROM student ORDER BY name
,因为name列上有索引,就不再出现filesort
:
8、Index merges
当WHERE
条件中有多个条件(或者join)涉及到多个字段,它们之间是 AND 或者 OR关系时,就有可能会使用到 index merge 技术。index merge 技术可以理解为:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union)。
index merge
根据合并算法的不同分成了三种:intersect
, union
, sort_union
intersect
是对多个索引条件扫描得到的结果进行交集运算,这显然是AND查询时才会出现的,例如:EXPLAIN SELECT user_id FROM test_order WHERE user_id=123 AND order_id=5
union
则是对多个索引条件扫描得到的结果进行并集运算,也就是OR查询:SELECT * FROM t1 WHERE key1=1 OR key2=2
,测试表中没出现该情形,可能是表中数据量太少,使用索引合并算法得不偿失,用一个数据量万级别的表测试,复现了场景。
Index merges
看起来效率不错,但也反映出我们索引有不合理之处,将条件中涉及的列建立联合索引,可以很好地优化。
这篇关于Index merges
的博文写得不错,值得看看:https://www.cnblogs.com/digdeep/p/4975977.html
想要优化SQL,找出效率低下的SQL是第一步,在这方面慢查询日志是有力的工具。
首先,通过一条语句查看当前数据库慢查询日志的情况:
SHOW VARIABLES LIKE '%slow_query_log%';
slow_query_log
:慢查询开启状态,ON为开启,OFF为关闭slow_query_log_file
:慢查询日志存放的位置查询到慢查询日志的状态后,可以使用命令进行修改(这种方式修改,Mysql服务器重启后会失效):
set global slow_query_log=on;
:打开慢查询日志set global long_query_time=1;
:设置记录查询超过多长时间的SQLset global slow_query_log_file='/tmp/slow_query.log';
:设置mysql慢查询日志路径,此路径需要有写权限set global log_queries_not_using_indexes=ON;
:设置没有使用索引的SQL记录下来如果想要设置永久生效,我们可以修改配置文件my.cnf
(可以通过find
命令查找,一般是/etc/my.cnf
),找到[mysqld]
,写入:
# 设置慢查询开启状态
slow_query_log =1
# 慢查询日志存放的位置
slow_query_log_file=/application/mysql/data/localhost-slow.log
# 询超过多少秒才记录 默认10秒 修改为1秒
long_query_time = 1
为了查看效果,我们使用select sleep(2);
语句人为制造一个慢查询,然后到慢查询日志中查看相关内容:
/opt/lampp/sbin/mysqld, Version: 3.5.16-log (Source distribution). started with:
Tcp port: 3306 Unix socket: /opt/lampp/var/mysql/mysql.sock
Time Id Command Argument
# Time: 180107 14:53:11
# User@Host: root[root] @ localhost [] Id: 1
# Query_time: 2.000754 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1515307991;
select sleep(2);
测试用表和数据奉上:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(25) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `c_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('1', '赵日天', '2020-07-28 20:22:40', '2020-07-28 20:22:40');
INSERT INTO `student` VALUES ('2', '叶良辰', '2020-07-28 20:23:29', '2020-07-28 20:23:29');
INSERT INTO `student` VALUES ('3', '龙傲天', '2020-07-28 20:24:13', '2020-07-28 20:24:13');
INSERT INTO `student` VALUES ('4', '徐胜虎', '2020-07-28 20:24:24', '2020-07-28 20:24:24');
-- ----------------------------
-- Table structure for class
-- ----------------------------
DROP TABLE IF EXISTS `class`;
CREATE TABLE `class` (
`id` int(11) NOT NULL,
`name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `s_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of class
-- ----------------------------
INSERT INTO `class` VALUES ('1', '三年1班');
INSERT INTO `class` VALUES ('2', '三年2班');
INSERT INTO `class` VALUES ('3', '三年3班');
-- ----------------------------
-- Table structure for class_student
-- ----------------------------
DROP TABLE IF EXISTS `class_student`;
CREATE TABLE `class_student` (
`id` int(11) NOT NULL,
`s_id` int(11) NOT NULL,
`c_id` int(11) NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `index_c_id` (`c_id`) USING BTREE,
KEY `index_s_id` (`s_id`) USING BTREE,
KEY `index_s_c_id` (`s_id`,`c_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of class_student
-- ----------------------------
INSERT INTO `class_student` VALUES ('1', '1', '1', '2020-08-03 16:53:02');
INSERT INTO `class_student` VALUES ('2', '2', '2', '2020-08-03 16:53:02');
INSERT INTO `class_student` VALUES ('3', '3', '2', '2020-08-03 16:53:02');