Mysql原理与调优-如何进行sql优化

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表示单表是如何访问的。类型有:

systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL

我们只需要了解常用的几个类型,并且类型性能排序如下:

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原理与调优-索引原理及使用一文中,我们介绍了如何利用索引来优化单表,本小结在补充一下复杂查询的优化内容。

  1. 对于排序分组等,尽量给排序字段或者分组字段建立索引,通过索引来实现分组和排序,减少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.查询时,尽量只查询需要查询的字段,原因是减少回表,尽量索引覆盖。

相关推荐
骆晨学长6 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
@月落12 分钟前
alibaba获得店铺的所有商品 API接口
java·大数据·数据库·人工智能·学习
楠枬22 分钟前
MySQL数据的增删改查(一)
数据库·mysql
goTsHgo27 分钟前
从底层原理上解释 clickhouse 保证完全的幂等性
数据库·clickhouse
hayhead1 小时前
高频 SQL 50 题(基础版)| 626. 换座位
sql·力扣
阿华的代码王国2 小时前
MySQL ------- 索引(B树B+树)
数据库·mysql
Hello.Reader2 小时前
StarRocks实时分析数据库的基础与应用
大数据·数据库
执键行天涯2 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
liupenglove2 小时前
golang操作mysql利器-gorm
mysql·golang
yanglamei19623 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask