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.查询时,尽量只查询需要查询的字段,原因是减少回表,尽量索引覆盖。

相关推荐
yuzhiboyouye3 分钟前
内连接,左连接,右连接怎么区别开来?
数据库
铭毅天下18 分钟前
Easysearch 版本进化全图——从 ES 国产替代到 AI Native 搜索数据库
大数据·数据库·人工智能·elasticsearch·搜索引擎
muddjsv25 分钟前
SQL 最常用技能详解与实战示例
数据库·sql·mysql
muddjsv2 小时前
大中小型企业数据配置年度成本估算分析
数据库·企业运营
ᰔᩚ. 一怀明月ꦿ2 小时前
MySQL 学习目标
学习·mysql·adb
塔能物联运维2 小时前
存量机房升级成为行业主流方向:热管理重构算力中心价值路径
数据库
lqj_本人2 小时前
鸿蒙electron跨端框架PC工志簿实战:项目、工时、阻塞和下一步都要有位置
数据库·华为·harmonyos
刘一说2 小时前
AI科技热点日报 | 2026年5月22日
数据库·人工智能·科技
LCG元3 小时前
RAG工程指南:从基础检索到生产部署全解析
java·运维·数据库
godspeed_lucip3 小时前
LLM和Agent——专题3: Agentic Workflow 入门(1)
大数据·数据库·人工智能