过年回来的第二周了,终于有时间继续总结知识了。这次来看一下SQL调优的知识,这类问题基本上面试的时候都会被问到,无论你的岗位是后端,运维,测试等等。
像本文标题中的两个问题,就是我在实际面试过程中遇到的,所以这次就主要围绕着这两个问题来总结一下。
本文的重点在第二部分,请耐心看完!
我们在想知道一条SQL的执行计划时,是可以通过Explain关键字来模拟优化器执行SQL查询语句,进而来分析SQL的语句。
创建如下数据表
CREATE TABLE `test_score` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '名称',
`subject` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '科目',
`score` int(10) DEFAULT NULL,
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_name_score` (`name`,`score`) USING BTREE COMMENT '学生名与成绩的联合索引',
KEY `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间的索引'
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=latin1 COMMENT='学生科目成绩表';
在上面的学生科目成绩表中,我创建了两个索引,一个是name和score的联合索引,一个是create_time的索引。
然后向test_score表中插入10w条数据,通过存储过程的方式添加:
DROP PROCEDURE IF EXISTS insert_test_score;
CREATE DEFINER = `root` @`%` PROCEDURE `insert_test_score` ( )
BEGIN
DECLARE
c_id INTEGER DEFAULT 1;
WHILE
c_id <= 100000 DO
INSERT INTO test_score
VALUES
( c_id, concat( '李明明', c_id ),
convertSubject(RAND()*10),
RAND()*100,
date_sub( NOW( ), INTERVAL c_id SECOND ) );
SET c_id = c_id + 1;
END WHILE;
END
自定义函数:convertSubject内容如下:
-- 自定义函数
delimiter $$
DROP FUNCTION IF EXISTS convertSubject $$
create function `convertSubject`(xis int) returns VARCHAR(50)
begin
CASE xis
WHEN 1 THEN RETURN '数学';
WHEN 2 THEN RETURN '语文';
WHEN 3 THEN RETURN "英语";
WHEN 4 THEN RETURN "物理";
WHEN 5 THEN RETURN "化学";
WHEN 6 THEN RETURN "生物";
WHEN 7 THEN RETURN "政治";
WHEN 8 THEN RETURN "历史";
WHEN 9 THEN RETURN "地理";
ELSE RETURN "体育";
END CASE ;
end
$$
delimiter ;
执行存储过程插入数据:
call insert_test_score();
数据插入成功后,我们查看一个查询SQL语句的执行计划:
EXPLAIN select * from test_score where name = '李明明5';
运行结果:
这里解释一下,Explain执行计划结果每个字段的含义:
id代表的主要是SQL语句的执行顺序,主要分两种情况:
如果只有一条SQL的单表查询(不含中间表)的情况,那么id就是1,如:
如果存在中间表查询的SQL,这时候会有多条执行结果,但是id值相同,这个时候代表的执行顺序是由上到下的顺序。
EXPLAIN select t2.name,t1.score
from test_score t1 join subject_score t2 on t1.id = t2.id where t2.id = 1;
EXPLAIN select `name`,score from test_score t1 where id = (SELECT id from test_score t2 where t2.id =1);
select_type代表的是查询类型,主要是区别于简单查询、联合查询、子查询等查询类型。
主要有以下几种类型:
数据输入行所引用的表名(若表名有别名,则展示别名)。
主要是显示连接类型,有如下几种:
指MySQL能使用哪个索引在该表中找到行,一般真正使用的索引都在possible_keys展示的索引中。
SQL在执行的时候实际走的索引名称,如果没有走索引,那么此值为Null。
表示索引中使用的字节数,该列计算查询中使用的索引的长度在不损失精度的情况下,长度越短越好。如果键是NULL,则长度为NULL。该字段显示为索引字段的最大可能长度,并非实际使用长度。
ref列显示使用哪个列或常数与key一起从表中选择行,如果值为const代表的是常数。
根据表统计信息以及索引选用情况,大致估算出找到所需的记录所需要读取的行数。
表示返回结果的行数占需读取行数的百分比 Filtered列的值越大越好 Filtered列的值依赖于统计信息。
指不适合在其他列中显示,但是十分重要的额外信息。
主要有如下几个值:
通过对Explain执行计划结果的了解,我们就可以在分析自己写的SQL时应该做哪些优化,这些操作是必须掌握的一些技能,还有就是要了解一下哪些情况下会造成索引失效,例如:对字段进行计算后查询或者是like ‘%***’,字段类型隐式转换等等。
还有就是尽量避免回表,能用覆盖索引完成查询最好,用到文件排序时,尽量避免产生filesort等等。
通过上面我们对Explain执行计划的各个字段内容的了解,我们知道了,MySQL在执行的SQL的时候,最终的一个执行计划是什么样的。
但是,不知道你们有没有遇到过这种情况,自己写了一个SQL,认为它会走某一个索引,结果却是走的全表扫描,没有走任何索引。还有就是,自己写的SQL,认为应该走A索引,但是通过Explain查看后,结果是走的B索引。
这些情况,其实是MySQL的查询优化器在对你的SQL进行分析后最终用了成本最小的执行计划。这说明有的时候MySQL认为扫描全表比走索引的成本更小。
那么MySQL的查询优化器是如何对SQL进行优化的呢?怎么就选出来了一个成本最低的策略呢。多个索引的时候,应该走哪个索引呢?
我们下面来慢慢分析。
MySQL在执行查询之前,会先对可能的方案做执行计划,然后在根据成本决定使用哪个执行计划。
这里说的成本是指:IO成本和CPU成本。
基于执行成本,我们来看一下,全表扫描的成本,还是基于上面的我们已经创建好的test_score表为例,通过如下SQL,查询出MySQL本身为每个表维护的统计数据。
SHOW TABLE STATUS LIKE 'test_score'
所以全表扫描的成本大概是19974+417=20391。
接下来,我们以一个SQL为例来说明一下,执行成本统计情况。
还是以上面我们已经创建好的数据表test_score为例。
EXPLAIN select * from test_score
where name > '李明明90017' and create_time < '2021-02-26 18:00:00';
上面这条SQL的执行计划结果是全表扫描:
但是只要我们把create_tieme的参数从18点改为17点,执行计划显示就会走索引了,并且走的是create_time的索引,而不是name字段的联合索引。
EXPLAIN select * from test_score
where name > '李明明90017' and create_time < '2021-02-26 17:00:00';
执行计划:
通过上面的例子我们可以看到,同样的查询字段,传入不同的值,有的就会走索引,有的确不走索引,并且MySQL选择的索引,也并不是根据where后面的查询字段的顺序来选择的。
产生这样结果的原因,就是MySQL基于成本来选择了最优的计划来执行了SQL。那么MySQL到底是怎么制定执行计划以及做出选择计划的依据是什么呢?
在MySQL5.6及之后的版本中,我们可以通过optimizer_trace
功能来查看优化器生成执行计划的整个过程。通过这个功能,我们可以了解MySQL每个计划的成本,然后来进一步对查询进行优化。
optimizer_trace
功能,默认是关闭的。可通过如下代码打开后,再执行具体的SQL,然后通过information_schema.OPTIMIZER_TRACE
表查看执行计划,最后记得手动再关闭optimizer_trace
功能。
SET optimizer_trace="enabled=on";
select * from test_score
where name > '李明明90017' and create_time < '2021-02-26 17:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
执行后的结果是个大JSON,内容比较多,所以我就只截取了关键部分。
我们先来看走idx_name_score
索引时执行计划:
我们看到,走idx_name_score
索引,要扫描数据21474行,成本(cost)是:25770。
再来看走idx_create_time时的执行计划:
这里看到,走idx_create_time
时,扫描数据6805行,成本(cost)是8167,比走idx_name_score索引成本要低。所以MySQL最终选择的是走idx_create_time
的执行计划。
最后再来将create_time的参数改为18点,然后看一下全表扫描的执行时间。
SET optimizer_trace="enabled=on";
select * from test_score where name > '李明明90017' and create_time < '2021-02-26 18:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
我们看到全表扫描要检索数据99869行,执行成本(cost)是20391。这个20391
正好是我们上面在统计全表扫描时计算出来的成本。
MySQL其实是将这三个执行计划成本进行比较,然后选取一个成本最小的,然后来执行。
通过optimizer_trace
功能,我们了解到了,MySQL是如何选择执行计划的,从而了解到MySQL是如何选择该使用哪个索引的。
其实,MySQL的执行计划选择,也并不是百分百准确的,有的时候,他计算出来的成本并不一定准确,所以我们可以强制让某条SQL使用指定的索引,还是拿上面的那条SQL举例:
-- 全表扫描,耗时3.545s
select * from test_score
where name > '李明明90017' and create_time < '2021-02-26 18:00:00';
-- 强制走索引,耗时3.088s
select * from test_score force index(idx_create_time)
where name > '李明明90017' and create_time < '2021-02-26 18:00:00';
好了,这次内容有点多,前面说Explain的部分有点太基础了,大部分人都会看,但是后面的optimizer_trace
相关的知识还是比较有意思的。
这里插入一个知识:通常我们在删除MySQL表中数据时都是使用delete操作,但是delete操作其实只是将数据该了状态,实际数据还是占用着磁盘空间呢。
所以要释放MySQL的表空间可以执行命令:OPTIMIZE TABLE + 表名
最后总结一下,我把Explain的各字段值的相关信息,做了一个表格。
optimizer_trace
相关的知识,这个就看个人兴趣了,知道有这么个功能就可以了,用到机会不是太多。
用上面的例子的test_score表,来分享一道我面试中遇到的SQL题。
还是科目成绩表,根据这张表,请用一条SQL写出来,每科成绩最高的同学。返回的字段要有同学的名称,科目和成绩。