MySQL的SQL性能分析是一个专业的JAVA开发人员无法逃避的知识,不管是面试还是在软件实际生产环境中,了解MySQL的SQL性能分析方法是非常重要的。
下面小郭对常见的MySQL性能分析排查思路进行了一个小结,分享给大家。
MySQL性能分析的水很深,本文也只是粗略介绍一些概念和方法,大家有补充的可以在评论区讨论一下哦!
1)创建测试表
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`company_id` INT(11) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2)定义存储过程,录入200W条测试数据
delimiter $$ # 定义结束符
drop procedure if exists addTestData; # 存储过程名叫:addTestData
create procedure addTestData()
begin
declare user_id int;
set user_id = 1;
while user_id <= 2000000 #插入N条数据
do
insert into user_info(user_id,name,company_id)
values(user_id,concat('用户_',CEILING(RAND() * 90000 + 10000)),1001); # 为了区分用户,我们在名称后加上后缀
set user_id = user_id + 1;
end
while;
end $$;
3)执行存储过程
call addTestData;
小郭这里插入了200W条测试数据,耗时2.5小时。。
4)删除存储过程(可选)
drop procedure addTestData;
该测试表用于后续的索引语法查找验证
如果我们想要查询id=5678的用户记录,执行如下sql:
SELECT user_id,name,company_id FROM user_info WHERE id = 5678;
上面查询是直接在聚簇索引上进行查找,执行结果是:1 row in set (0.00 sec)
我们看看上面sql的查询成本,执行如下命令:
mysql> SHOW STATUS LIKE 'last_query_cost';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| Last_query_cost | 1.000000 |
+-----------------+-----------+
可以得出结论,实际上耗时0秒(实际可能是毫秒级别,也许几十ms),我们只需要检索一个页即可得到目标数据。
MySQL中可以通过命令show status like 'last_query_cost' 来统计SQL的查询成本,它是io_cost和cpu_cost的开销总和,它通常也是我们评价一个查询的执行效率的一个常用指标。
(1)它是作为比较各个查询之间的开销的一个依据。
(2)它只能检测比较简单的查询开销,对于 包含子查询和union的查询是测试不出来的。
(3)当我们执行查询的时候,MySQL会自动生成一个执行计划,也就是query plan,而且通常有很多种不同的实现方式,它会选择最低的那一个,而这个cost值就是开销最低的那一个。
(4)它对于比较我们的开销是非常有用的,特别是我们有好几种查询方式可选的时候。
如果我们是想查询id范围在(1000,1100)的记录,执行sql如下:
SELECT user_id,name,company_id FROM user_info WHERE id >= 1000 AND id <= 1100;
上面查询页是直接在聚簇索引上进行查找,执行结果是:101 row in set (0.01 sec)
我们看看上面sql的查询成本,同样执行如下命令:
mysql> SHOW STATUS LIKE 'last_query_cost';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| Last_query_cost | 41.734363 |
+-----------------+-----------+
可以看出,范围查询sql查询页的数量是前面单个查询sql的41倍,但实际上这两个sql的执行效率没有太大差别,这其实是因为缓冲池的作用。
虽然 页 数量(last_query_cost)增加了不少 ,但是采用了顺序读取(非随机读取)的方式将页面一次性加载到缓冲池中,并 不会增加多少查询时间 。
show profile 是 MySQL 提供的可以用来分析当前会话中 SQL 都做了什么、执行的资源消耗工具的情况,可用于 sql 调优的测量。默认情况下处于关闭状态
,并保存最近15次的运行结果。
我们需要先将profiling参数打开,执行命令 :
set profiling = 'on'
然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:
show profiles;
通过上面的图可以看到当前会话一共有 3 个查询。如果我们想要查看最近一次查询的执行成本开销,可以使用show profile:
show profile;
show profile 也可以指定参数,如 cpu、block io等:
show profile cpu,block io for query 2;
show profile的常用查询参数有下面这些:
参数 | 作用 |
---|---|
BLOCK IO | 显示块IO开销 |
ALL | 显示所有的开销信息 |
CPU | 显示CPU开销信息 |
CONTEXT SWITCHES | 上下文切换开销 |
IPC | 显示发送和接收开销信息。 |
MEMORY | 显示内存开销信息 |
PAGE FAULTS | 显示页面错误开销信息 |
SOURCE | 显示和Source_function,Source_file, Source_line相关的开销信息 |
SWAPS | 显示交换次数开销信息 |
经验之谈:
如果执行show profile查询的结果列status出现如下信息的任何一条,则大概率需要进行SQL语句优化了。
Status中的信息 | |
---|---|
Creating tmp table | 创建临时表。先拷贝数据到临时表,用完后再删除临时表 |
converting HEAP to MyISAM | 查询结果太大,内存不够,数据往磁盘上搬了 |
Copying to tmp table on disk | 把内存中临时表复制到磁盘上,警惕! |
locked | 发生锁的情况 |
1)开启 slow_query_log
MySQL提供了slow_query_log
参数可以开启统计慢查询sql信息,但是默认是不开启该功能的,通过下面命令可以查看是否开启:
show variables like '%slow_query_log'
可以看到 slow_query_log=OFF
,我们需要把慢查询日志打开:
//注意设置变量值的时候需要使用 global,否则会报错:
set global slow_query_log='ON';
2)设置 long_query_time 阈值
先查询下long_query_time 参数的默认配置:
show variables like '%long_query_time'
可以看出针对参数long_query_time默认配置的是10秒,也就是说执行超过10秒的sql才肯会被判断为慢SQL, 这里我们把参数设置为1秒
set global long_query_time = 1;
注意:控制慢查询日志的还有一个系统变量: min_examined_row_limit。
这个变量的意思是查询扫描过的最少记录数。
这个变量和long_query_time共同作为了判断一个 查询语句是否是慢查询的条件。 也就是说:如果查询扫描过的记录数大于等于min_examined_row_limit,且查询执行时间超过long_query_time,则这个查询语句就被认为是慢查询,记录到慢查询日志中,否则不被记录。 min_examined_row_limit的系统默认值是0,也就是说如果我们没有主动设置过这个值,查询语句只需要满足执行时间大于long_query_time即被认定为是慢查询。
然后通过show命令查看配置的值是否生效:
SHOW variables LIKE '%long_query_time';
我们会发现怎么还是10呢?如果执行show命令时带上global参数:
SHOW global variables LIKE '%long_query_time';
会发现值其实是生效的:
这里需要注意一点:设置global的方式对当前session的long_query_time不会失效,只对新连接的客户端有效。
因此如果我们不重新连接mysql的话,需要同时执行下面命令(不带global),才可以使参数设置对当前连接有效:
set long_query_time=1;
首先查看系统目前监控到的慢查询语句的数量:
show global variables like '%slow_queries'
如果查询显示大于0,则说明存在慢SQL, 找到慢SQL记录文件所在路径:
SHOW VARIABLES LIKE 'slow_query_log%';
可以看到默认配置的慢SQL记录保存在D:\mysql-5.7.43-winx64\data\ace-slow.log (这是小郭安装mysql程序的目录),我们找到这个ace-slow.log文件就能看到慢查询语句是哪些了。
如上图所示,文件中有记录call addTestData这条执行语句,本文最开始的部分【准备工作】说过这条存储过程执行耗时了2.5h,是肯定符合慢SQL判定条件的!
一般我们的生产环境系统不太会直接使用到存储过程,该文件中会记录的大部分都是一些常见的CRUD表操作,只要定位到某条语句是慢查询,接下来就可以针对这条语句进行具体的分析优化了。
Explain工具的官方完整文档: MySQL5.7 MySQL8.0
通过前面的铺垫,我们已经定位到了具体的慢SQL语句了,接下来可以使用MySQL提供的Explain工具(Describe工具等价)对该语句做具体问题分析了。 通过Explain工具可以查看到某个SQL语句的具体执行计划,了解Explain工具的分析结果,比如对每个表采用的是什么访问方法,走了哪个索引查询,多表连接的顺序等等信息,可以引导我们针对性的优化SQL。
执行计划是MySQL中专门负责优化SELECT语句的模块来定义的,该模块会经过一系列分析最终定它认为最优的执行计划,然后交给执行模块去执行。
Explain工具支持的SQL类型:
EXPLAIN的语法形式如下:
EXPLAIN SQL语句
示例:
explain select user_id from user_info limit 1
如上图所示就是MySQL的SQL优化器模块给出来的执行计划。执行explain并没有真正的去执行SQL语句,不会对数据产生实际影响,只是确定一个待执行的执行计划,如果后面提交执行就是使用该执行计划去落地。
通过执行计划,我们可以了解到我们SQL是否可以改善得更加的高效。除了SELECT,DELETE、INSERT、REPLACE、UPDATE等语句都可以用explain进行执行计划的预览。
执行计划输出的字段清单如下:
字段 | 概述 |
---|---|
id | 每个SELECT语句中都对应一个唯一的id |
select_type | SELECT 关键字对应的查询类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的访问方法 |
possible_keys | 可能命中的索引 |
key | 实际命中的索引 |
ken_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 额外的信息 |
下面小郭将对其中重要字段的用途进行详细的总结说明。
1)id
查询语句一般都以 SELECT 关键字开头,但是一个SQL语句中可能出现一次SELECT或者多次SELECT关键字,这两种情况下的id生成有区别。 查询语句中每出现一次SELECT关键字,MySQL就会为它分配一个唯一的id值,这个id是explain执行结果的第一列。
下边是一个比较简单的查询语句,只出现了1次SELECT:
SELECT user_id from user_info limit 1;
使用explain 对上边的sql进行执行计划预览:
EXPLAIN SELECT user_id from user_info limit 1;
可以看出,mysql为这条sql的select生成的唯一值id是1。
再看一个多表连接的例子,这个例子出现了2次SELECT:
EXPLAIN SELECT a.user_id,b.name from user_info a inner join user_info2 b where a.user_id = b.user_id and a.user_id = 1;
从上图可以看到, 连接查询时出现了两次select关键字,对应执行计划中生成了两条记录,但是他们的id值是相同的。 这是MySQL内部的特殊处理,在多表连接查询的执行计划中,每个表都会对应一条执行计划的记录,且这些记录的id列的值是相同的。出现在前边的表表示驱动表
,出现在后面的表表示被驱动表
。所以从上边的EXPLAIN输出中我们可以看到,查询优化器准备让b表作为驱动表,让a表作为被驱动表来执行查询(这里的a和b是sql中定义的表的别名)。
接着看一个使用union联合查询的例子,也出现了两次SELECT:
EXPLAIN SELECT user_id,name from user_info
UNION
SELECT user_id,name from user_info2 ;
从上图可以看到,explain生成的执行计划中出现了三行记录,首先针对每个select关键字生成了两个唯一id,还生成了一个id=null的数据。这里的null值id是具有特殊含义的:在MySQL中,union关键字会把多个结果集合并进行去重再返回给调用方。这个id=null的记录就是为了方便返回最终查询结果前去重的,MySQL在内部创建了一个名为的临时表(就是执行计划中第三条记录的table列的名称),id为null表明这个临时表是为了合并两个select查询的结果集而创建的。
注意MySQL中除了支持去重的联合查找union,还支持不需要去重的联合查找union all。因为不需要去重,所以用不到临时表,也就是说,在执行计划中不会生成id=null的记录。实践是检验真理的唯一标准,我们可以执行union all进行验证一下:
EXPLAIN SELECT user_id,name from user_info
UNION ALL
SELECT user_id,name from user_info2 ;
从上图可以看出,union类型的sql在执行计划中生成了3条记录,其中有一条id为null的记录,而union all类型的sql只生成了2条记录。
2)select_type
类型 | 解释 |
---|---|
SIMPLE | 查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型, 连接查询join也是SIMPLE类型。 |
PRIMARY | 对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type的值就是PRIMARY,也就是说驱动表的select_type是primary,例如:EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2; |
UNION | 对于包含UNION或者UNION ALL的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询意外,其余的小查询的select_type值就是UNION |
UNION RESULT | MySQL 选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT, |
SUBQUERY | 如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是SUBQUERY,比如下边这个查询:EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a'; |
DEPENDENT SUBQUERY | 示例:EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a'; |
DEPENDENT UNION | 示例:EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b'); |
DERIVED | EXPLAIN SELECT * FROM (SELECT key1, count(*) as c FROM s1 GROUP BY key1) AS derived_s1 where c > 1; |
MATERIALIZED | 当查询优化器在执行包含子查询的语句时,选择将子查询物化之后的外层查询进行连接查询时,该子查询对应的select_type属性就是DERIVED,比如下边这个查询:EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2); |
UNCACHEABLE SUBQUERY | 基本不用 |
UNCACHEABLE UNION | 基本不用 |
3)table
不管我们的SQL语句有多复杂,到最后MySQL都是需要对每个表进行单表访问的,因为MySQL中每个表的数据存储都是单个的文件。
所 以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该 表的表名(有时不是真实的表名字,可能是简称)。EXPLAIN语句输出多条记录,table列就会有多个值。
4)partitions
代表分区表中的命中情况,非分区表,该项为NULL
。一般情况下我们的查询语句的执行计划的partitions
列的值为NULL
。
下面演示分区表的使用。
创建分区表,该表按照id分区,id<100 p0分区,其他p1分区:
CREATE TABLE user_info_partitions (id INT auto_increment,
NAME VARCHAR(12),PRIMARY KEY(id))
PARTITION BY RANGE(id)(
PARTITION p0 VALUES less than(100),
PARTITION p1 VALUES less than MAXVALUE
);
查询id < 100的记录,看看分区表的命中情况:
explain SELECT * FROM user_info_partitions WHERE id<100;
可以看出,命中了分区p0。
查询id>100的记录,看看分区表的命中情况:
SELECT * FROM user_infopartitions WHERE id>100;
可以看出,命中了分区p1。
5)type
执行计划中的type是我们分析慢SQL时要重点关注的。该字段代表着MySQL对某个表的 执行查询时的访问方法
, 又称“访问类型”,比如,看到type
列的值是ref
,表明MySQL
即将使用ref
访问方法来执行对xxx表的查询。
type的可选值有很多: system , const , eq_ref , ref , fulltext , ref_or_null , index_merge , unique_subquery , index_subquery , range , index , ALL
。
因为type这个属性比较重要,小郭来对type的部分重要可选值来详细演示说明一下。
当表中只有一条记录
并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system
。我们新建一个存储引擎是MyISAM
类型的表,并为其插入一条记录 ,最后使用explain分析一下执行计划:
此时,如果我们继续插入记录,再使用explain分析执行计划,type就不会是system类型了
或者我们把表的存储引擎改为INNODB, 再使用同样的sql查询,explain工具生成执行计划type也会是all,有兴趣的小伙伴可以自己尝试一下。
当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const
, 比如:
//创建表t2,id是主键列
create table t2(id int primary key) engine =INNODB;
//插入一条记录
insert into t2 values(1);
//使用explain进行查询语句的分析
explain select *from t2 where id = 1;
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(被驱动表和驱动表的概念在文章开头有介绍),则对该被驱动表的访问方法就是eq_ref
,下面演示一下这种类型何时会出现:
//创建表t3和表t4, 并分别插入一条数据,然后使用连接查询两表的数据(假设两个表的主键id是关联条件)
//注意:这里是演示效果,实际业务中不会出现两个主键id互为关联条件哦
create table t3(id int primary key);
create table t4(id int primary key);
insert into t3 values(1);
insert into t4 values(1);
explain select *from t3 inner join t4 on t3.id = t4.id;
从上图中执行计划的结果中可以看出,MySQL打算将t3作为驱动表,t1作为被驱动表。t1的访问 方法是 eq_ref
,表明在访问t1表的时候可以 通过主键的等值匹配
来进行访问。
//创建表t5,其中的字段name是二级索引(非主键索引)
create table t5(id int primary key, name varchar(100), index idx_name(name));
insert into t5 values(1,'test');
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
。
explain select *from t5 where name = 'test';
当通过普通的二级索引列与常量进行等值匹配,,该索引列的值也可以是NULL
值时,那么对该表的访问方法就可能是ref_or_null
。
explain select *from t5 where name = 'test' or name is null;
一般情况下对于某个表的查询只能使用到一个索引,但单表访问方法时在某些场景下可能会同时用到多个索引。
我们执行下面的查询语句:
explain select *from t5 where name = 'test' or id = 1;
从explain生成的执行计划结果可以看出,使用主键索引和二级索引进行同时检索(实际不会发生这种情况,因为主键索引就可以唯一定位记录了) ,type的值就是index。
上面总结了MySQL中慢SQL的几种排查手段,都涉及到MySQL服务的一些全局的基本配置项修改。而然在实际生产环境,修改任何MySQL全局配置都需要慎重!
转载链接:https://www.zhihu.com/question/388019803/answer/3207496688