1.绪论
本文主要讲解我们如何优化一个sql。优化的过程主要分为3个步骤,找到哪些sql需要被优化,这就需要用到慢sql日志。然后发现慢SQL为什么慢,即当前sql是如何执行的,这就需要用到执行计划。最后才是对sql进行优化,对于开发而言,一般是不会去调节Mysql服务的各种参数的,所以一般是从索引的角度对sql进行优化。
2.哪些sql需要优化-慢sql日志
2.1 什么是慢sql日志
当执行时间超过某个阈值或者某些sql未走索引,会被加入到慢sql日志中。
2.2 慢sql的相关参数
|-----------------------------------|----------------------|-----------------|
| 参数 | 说明 | 备注 |
| slow_query_log | 是否开启慢sql日志0-不开启,1开启 | 默认不开启,开启可能会影响性能 |
| slow_query_log_file | 慢日志文件位置 | |
| long_query_time | 超过多少秒算慢sql | 一般设置为1秒 |
| log_queries_not_using_indexes | 未走索引的sql是否会加入到慢日志文件中 | |
3.慢sql现在是如何执行的-执行计划
3.1 数据准备
我们准备两张表分别是用户基础信息表tb_user和用户表tb_user_info。其中tb_user表示用户创建注册过系统便会在改变留下一条记录。而tb_user_info表示是用户对系统的使用信息。
sql
CREATE TABLE `tb_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`phone` varchar(11) NOT NULL COMMENT '手机号码',
`password` varchar(128) DEFAULT '' COMMENT '密码,加密存储',
`nick_name` varchar(32) DEFAULT '' COMMENT '昵称,默认是用户id',
`icon` varchar(255) DEFAULT '' COMMENT '人物头像',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniqe_key_phone` (`phone`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1010 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
sql
CREATE TABLE `tb_user_info` (
`user_id` bigint(20) unsigned NOT NULL COMMENT '主键,用户id',
`city` varchar(64) DEFAULT '' COMMENT '城市名称',
`introduce` varchar(128) DEFAULT NULL COMMENT '个人介绍,不要超过128个字符',
`fans` int(8) unsigned DEFAULT '0' COMMENT '粉丝数量',
`followee` int(8) unsigned DEFAULT '0' COMMENT '关注的人的数量',
`gender` tinyint(1) unsigned DEFAULT '0' COMMENT '性别,0:男,1:女',
`birthday` date DEFAULT NULL COMMENT '生日',
`credits` int(8) unsigned DEFAULT '0' COMMENT '积分',
`level` tinyint(1) unsigned DEFAULT '0' COMMENT '会员级别,0~9级,0代表未开通会员',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
3.2 什么是执行计划
Mysql的优化器在根据各种成本计算过后,会为该条sql生产一个执行计划。这个执行计划包含了表的驱动关系,最终执行的索引,大概得扫描行等。根据,执行计划,我们可以知道执行器最终是如何执行sql的,选择的索引是什么或者索引失效的原因是什么。我们可以通过explain关键字查询执行计划。
sql
explain sql语句
3.3 explain查询结果详解
sql
EXPLAIN SELECT * FROM tb_user
通过执行explain,我们可以得到如下信息,接下来我们就对每个字段进行解释。
3.3.1 id
sql语句每个select关键字就会生成一个id,针对id主要讲3种情况。
1.连接查询
连接查询id应该是一样的,但是上面的为驱动表,下面的为被驱动表。
sql
explain SELECT * FROM tb_user u left join tb_user_info ui on u.id = ui.user_id
2. 子查询
子查询有多个select所以有多个id,id越大,越先执行。
sql
EXPLAIN
SELECT * FROM tb_user WHERE id IN (
SELECT id WHERE nick_name='tom'
)
3. union
union会在临时表中去重,相当于再次执行了selct操作,但是在临时表中去重这一操作的id为null。
sql
EXPLAIN
SELECT id FROM tb_user WHERE nick_name='tom'
UNION
SELECT id FROM tb_user WHERE nick_name='jack'
3.3.2 select_type
select _type其实就是对于每个select的定义的属性,表示该select操作具体是一个什么类型的操作,一般和id是一一对应的。
1.simple
只要不包含子查询,union,union all便是simple。
sql
explain select * from tb_user
2.union 和union all
union查询第一个查询类型为priamary,后面的查询类型为union,如果基于他们的结果在临时表中去重类型为union result。
3.子查询
子查询的外层查询为primary,内层查询为subquery
sql
EXPLAIN
SELECT * FROM tb_user WHERE id IN (
SELECT id WHERE nick_name='tom'
)
4. 物化表
Mysql会把一些sql的查询结果,持久化到磁盘上,形成物化表。对于物化表的type为DERIVED。
sql
EXPLAIN
SELECT * FROM (
SELECT id,COUNT(id) AS num FROM tb_user GROUP BY id
) tmp
3.3.3 partitions
分区,mysql可以将数据持久化文件分别存储到不同的磁盘文件上,提升磁盘IO效率。partitions展示的就是分区信息,一般为空。
3.3.4 type
type表示单表是如何访问的。类型有:
system
,const
,eq_ref
,ref
,fulltext
,ref_or_null
,index_merge
,unique_subquery
,index_subquery
,range
,index
,ALL
我们只需要了解常用的几个类型,并且类型性能排序如下:
system> const>ref> ranget>index>all
1. system
表中只有一条数据时,为system。
2.const
唯一索引并且等值访问。(注意,唯一索引里面可能为null,此时用is null会退化为ref)。
3.eq_ref
连接查询,被驱动表走唯一索引。
4. ref
走二级索引访问。
5. ref_or_null
如果走二级索引并且字段可能为null,可能为ref_or_null。
5.index_merge
index_merge主要包含Intersection、Union合并和Sort-Union合并。
一般出现这种的sql语句:
1) 什么是索引合并
主要是where条件有多个条件,条件之间or或者and连接,条件针对的是不同的列并且每个列都有索引。如果是and连接可能会出现Intersection,如果用or连接,可能出现union或者Sort-Union。
2) Intersection
sql
SELECT * FROM tb_user WHERE phone = '13688668922'
AND nick_name='user_p3655ctliy'
Intersection的执行步骤:
1.如果不采用索引合并,sql该怎么执行呢?上面sql语句可能会先走idx_phone这个索引,获取到phone=13688668922的主键id,然后到主键索引回表,过滤得到nick_name='user_p3655ctliy'的数据。
2.如果采用索引合并,应该怎么执行呢?其实就是取出的走idx_phone这个索引,获取到phone=13688668922的主键id的,然后走idx_nick_name获取到nick_name='user_p3655ctliy'的主键id,取交集,然后根据主键id交集到主键索引中回表,得到数据。
要执行Intersection满足哪些条件:
1.如果走的是非主键索引(可能是联合索引),必须是等值查询,并且索引中的每列都出现在where条件中。这是因为,如果存在某列不在的where条件中,查询出来的主键id可能是一个范围,与另一个索引得到的主键id取交集时,可能性能很低。
2.如果是主键索引,可以为范围查询。原因是,在intersection的时候,可以走二级索引获取到主键id,然后回表时根据主键id范围过滤即可。
**3)**Union合并
如果多个where条件采用or进行连接,可能会出现or的情况。union查询步骤和intersection的查询步骤是一样的,只是union查询是查出每棵索引树的id,然后取并集,最后回表。
4)索引合并的优化
索引可以通过给where条件后面的列建立联合索引进行优化。
6.unique_subquery
子查询走的是唯一索引进行连接。
7.index_subquery
子查询走非唯一索引进行连接。
8.range
如果有范围查询,或者in子句走索引,为range。
9.index
没有走索引,但是索引上面的列包含了where条件中的列,可以通过索引来进行过滤。
sql
ALTER TABLE tb_user ADD INDEX idx_phone_create(`nick_name`,`create_time`);
EXPLAIN SELECT * FROM tb_user WHERE create_time = '2022-02-28 10:50:47'
10.All
全表扫描。
从上面可以看出,如果索引为index或者All,这个时候就已经没有走索引了,我们就需要考虑如果优化。
3.3.5 passible key和key
优化器会根据passible key里面的索引和全表扫描进行成本比较,最后会选择成本最低的key做为执行路径。
我们如果想查看优化器是如何锁定key最后最后的计划的,可以打开optimizer_trace:
sql
SHOW VARIABLES LIKE 'optimizer_trace' //查看optimzer_trace是否打开
SET optimizer_trace="enabled=on"; //打开optimizer_trace
SELECT * FROM tb_user WHERE phone = '13688668922' AND nick_name='user_p3655ctliy';
SELECT * FROM information_schema.OPTIMIZER_TRACE; //查看优化过程
3.3.6 key_len
key_len表示的是索引字段的字节数,可以用它来判断联合索引被使用了几列:
1.如果是定长字段,key_len就是改字段占用的字节数;
2.如果是变成字段,为该字段所能包含的最大字节数,比如varchar(10)且采用utf8编码,则为key_len为30;同时,会多两个字节来存储变成字段的长度。
3.如果索引可以为null,比部位null多一个字节。
3.3.7ref
如果作等值匹配时,并且走索引,ref表示的是,做等值匹配的是什么内容。比如const就是一个常数。
3.3.8 rows
rows代表该执行计划最后需要扫描的行数。如果,扫描函数过大,但是又走了索引,此时就需要考虑花在回表上的成本。
3.3.9 filter
filter表示走当前索引后,满足最终条件的行数,占通过当前索引过滤出来的行数的百分比。我们举个例子。
sql
explain SELECT * FROM tb_user WHERE id > 80 AND nick_name='user_p3655ctliy'
可以看出,前面SELECT * FROM tb_user WHERE id > 80 会走主键索引过滤,得到大概扫描行数929行,然后再在这929行中,进行过滤出nick_name='user_p3655ctliy'的记录,优化器估这里又92.9行满足条件。
3.3.10 extra
extra是explain中很关键的一个字段,里面包含了这个sql具体使用的优化技术。
1.useing index
表示索引覆盖,不需要回表。
2. using index condition
表示使用索引下推。
sql
EXPLAIN SELECT * FROM tb_user WHERE nick_name LIKE 'j%'
AND nick_name LIKE '%a%'
如果未使用索引下推之前,这种多个条件不能形成范围的条件,怎么执行呢?
1.通过二级索引查询出以j开头的记录的主键id;
2.通过主键id到主键索引回表,查询出完整记录返回给server层;
3.server层根据主键索引过滤出nick_name包含a的记录。
如果使用索引下推:
1.通过二级索引查询出以j开头的记录;
2.由于该二级索引包含另一个条件的字段,所以可以直接在索引里面进行判断,得到包含的字段a的记录的主键id;
3.到主键索引,进行回表返回完整记录。
3. using where
表示先走索引,再回表后将记录返回给server层,server对剩余条件进行过滤。
4.using join buffer
表示连接查询的时候,会采用join buffer来进行优化。在连接查询的时候,其实就类似于二重for循环,针对这个二重for循环,Mysql页提出了很多优化手段。我们看看下面这条语句是如何工作的:
sql
SELECT * FROM tb_user u LEFT JOIN tb_user_info ui ON u.id = ui.user_id
1) Simple Nested-Loop Join( 简单的嵌套循环连接)
其实就是先根据where条件过滤出tb_user中的数据,然后的取出连接值,依次到的tb_user_info中去查询。
2) Index Nested-Loop Join( 索引嵌套循环连接 )
索引嵌套查询其实就是驱动表取出连接值,然后到被驱动表的索引进行匹配。
3) lock Nested-Loop Join( 块嵌套循环连接 )
块嵌套查询就是依次从驱动表中获取的多条数据,到join buffer中(这样可以减少驱动表的磁盘IO操作),然后一次性到被驱动表中进行匹配。
4) 连接查询优化
1.小表驱动大表,原因主要是驱动表在走where条件后,再过滤的数据全部扫描一遍,而被驱动表可以走索引,所以被驱动表越小越好;join buffer大小是有限的也可以作为另一个原因。
2.尽量给被驱动表的连接字段建立索引。
5.using filesort
在order by,distinct,group by的时候,会默认进行排序。如果不能走索引的话,会将数据加载到sort buffer中,利用排序算法进行排序。这个过程是比较耗时的。因为索引是天然有序的,所以我们尽量利用索引进行排序。
注意:Group by也会带着排序。
如果要禁用掉分组的排序,我们可以在后面加上order by null。
sql
explain select nick_name ,count(id) as num from tb_user
Group by nick_name order by null
6.Using intersect ,using union
表示采用了索引合并
7.Using temporary
表示采用了零时表,如果执行了union,distict,group by等操作的时候,可能建立内部临时表来进行查询。
4.如何优化sql
在Mysql原理与调优-索引原理及使用一文中,我们介绍了如何利用索引来优化单表,本小结在补充一下复杂查询的优化内容。
- 对于排序分组等,尽量给排序字段或者分组字段建立索引,通过索引来实现分组和排序,减少filesort和temporary。
2.对于连接查询,尽量使用小表驱动大表,并且给连接字段建立索引。原因,前文已经分析过。
3.对于in和exists的选择,同连接查询类似。select A in(select B),会先执行B表查询,再执行A表查询,所以B表应该尽量小,并尽量给A表的过滤字段建立索引。select A exists (select B),会先执行A表,再到B中判断结果是否存在,所以A表应尽量小,同时给B表的连接字段建立索引。
3.查询时,尽量只查询需要查询的字段,原因是减少回表,尽量索引覆盖。