当我们碰到MySQL的一些异常或者问题时,应该要有全局观,这样能够帮助你理解问题,更为快速地定位并解决问题。
下面我给出的是MySQL的基本架构示意图,从中你可以清楚地看到SQL语句在MySQL的各个功能模块中的执行过程。
通过以上图片,我们清楚地看到SQL语句在MySQL的各个功能模块中的执行过程。但和sql执行效率相关的主要是优化器和执行器,同时查询缓存也有一定的影响作用。
查询缓存(适用于查询语句)
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。
需要注意的是,MySQL 8.0版本直接将查询缓存的整块功能删掉了,也就是说8.0开始彻底没有这个功能了。
优化器(索引影响执行效率)
为了方便说明问题,这里先给出建表语句:
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL PRIMARY KEY,
`c` int(11) DEFAULT NULL,
`d` varchar(20) DEFAULT NULL,
) ENGINE=InnoDB;
1.字段没有索引
例如你要查询这条语句:
select * from t where 100
刚好你的 c 字段上没有索引,那么抱歉,只能走全表扫描了,你就体验不会索引带来的乐趣了,所以,这回导致这条查询语句很慢。
2.字段有索引,但用索引失效
(1)运算操作导致没有用上索引
给 c 这个字段加上了索引,然后又查询了一条语句
select * from t where c - 1 = 1000;
正确的查询应该如下:
(select * from t where c = 1000 + 1;
为什么对索引字段进行了计算操作之后,就不会走索引了呢?
这里我们需要明白,走索引的本质其实是利用了B+树的有序性以便进行快速定位。但是对索引字段进行计算操作之后,有可能会破坏这种有序性(非线性计算),导致无法利用B+树的这一特性,因此优化器会放弃使用B+树的树搜索功能。
(2)函数操作导致没有用上索引(原因同运算操作)
如果我们在查询的时候,对字段进行了函数操作,也是会导致没有用上索引的,例如
select * from t where pow(c,2) = 1000;
(3)like操作
在like操作中,当%在前面时,也不会走索引。
select * from t where d like"%1";
(4)隐式转换导致索引失效.
在MySQL中,每张表都可以单独指定其字符集,最常见的就是utf8和utf8mb4了,当两张表字符集不同时,进行联表操作会存在字符集转换,从而导致索引失效。
错误的例子:
select * from t where d=1;
正确的例子:
select * from test t d='1';
(5)not in 操作
MySQL5.6之前的版本,not in操作也会导致索引失效。但在MySQL5.6引入ICP优化之后,not in操作也是可以走索引的,请看:
select * from t where c not in (1,2);
所谓ICP,全称Index Condition Pushdown(索引条件下推),即在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不满足条件的记录,如此一来就可以减少回表次数,提升性能。
关于索引失效还有许多,这里就不详细介绍了,具体参考索引失效原因总结 - 梦中山河 - 博客园
3.扫描行数太多
select * from t where 100 < c and c < 100000;
当访问的数据占全表数据较大时,优化器会放弃辅助索引而直接全表扫描。
我们知道选择索引时优化器的工作,优化器在决定使用哪个索引时会综合考虑扫描行数、是否使用临时表、是否排序等信息,在我们这个例子中显然就是扫描行数的原因。
那么在执行语句之前,优化器是怎么知道扫描行数的呢?这里就需要提到索引区分度了,也就是索引Cardinality(基数),这是一个统计信息,用于统计索引中不重复记录的个数。我们可以用show index命令看下。有时候你会看到主键的基数比行数还多,这是什么原因?
因为基数本身其实是一个预估值,如果通过全表扫描的方式来精确统计的话,每次发生变更都需要做一次全表扫描,在生产环境这是不可接受的。所以InnoDB是通过采样的方式来预估的,既然是预估,那么就有可能不准确。
因此,当基数统计不准确的时候,优化器就有可能不走索引或者选错索引,针对这种场景,我们有三种解决方案:
不过呢,我们有时候也可以通过强制走索引的方式来查询,例如
select * from t force index(a) where c < 100 and c < 100000;
我们也可以通过:
show index from t;
来查询索引的基数和实际是否符合,如果和实际很不符合的话,我们可以重新来统计索引的基数,可以用这条命令
analyze table t;
来重新统计分析。
4.主键索引和非主键索引
为了说明这个我们再创建一个表,这个表的建表语句是:
mysql> create table T
( `id` int primary key,
`k` int not null,
`name` varchar(16),
index (k))engine=InnoDB;
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6),两棵树的示例示意图如下。
从InnoDB的索引组织结构从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引(clustered index)。非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)。
根据上面的索引结构说明,我们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
执行器
我们还是从一个表的一条更新语句说起,下面是这个表的创建语句,这个表有一个主键ID和一个整型字段c:
mysql> create table T(ID int primary key, c int);
如果要将ID=2这一行的值加1,SQL语句就会这么写:
update T set c=c+1 where ID=2;
这里我给出这个update语句的执行流程图,图中浅色框表示是在InnoDB内部执行的,深色框表示是在执行器中执行的。(与查询流程不一样的是,更新流程还涉及两个重要的日志模块,即:redo log(重做日志)和 binlog(归档日志)。)
在这些过程中redo log以及事务进行状态下遇到的锁也会影响sql的执行效率
1.刷脏页
为了保证事务的持久性,InnoDB引擎采用了Write Ahead Log(WAL)策略,即事务提交时,先写日志,再写磁盘。当然在写日志之前会更新内存,而这里的日志自然也就是redo log了。
当内存中的数据页相对磁盘的数据页发生变化时,我们称该内存页为脏页,反之则为干净页。从持久性上考虑,脏页是必须要刷回磁盘,这个过程我们称之为刷脏页,而这个过程中就有可能导致平时执行很快的SQL突然变慢了。那么什么情况会触发MySQL的刷脏页操作呢?一般来说有以下4种情况:我们知道redo log区别于binlog的一个特点是容量有限、循环写入。因此当redo log写满时,必须停止其它所有更新操作来将redo log写入磁盘
因为脏页是存在于内存中,因此当内存不够用时,需要淘汰一部分数据页,如果淘汰的是脏页,就需要先将脏页同步到磁盘
MySQL认为系统比较空闲的时候
MySQL正常关闭的时候
而在这4种情况中,对我们影响最大的就是redo log写满的场景,因为一旦出现这种情况,整个系统将不再接受更新,这种场景需要尽量避免。另外如果内存不够用,需要淘汰的脏页太多,也会明显影响性能。所以控制好脏页刷新的机制很重要。
2.等待锁
我们正在执行的SQL语句,可能需要对某些资源加锁(比如表锁或者行锁),但是该资源正在被其他事物持有,导致当前事务必须等待。碰到这种场景,我们可以使用show processlist命令查看当前语句的状态,以作进一步的分析。
总结:
一个 SQL 执行的很慢,我们要从sql执行过程进行分析:
1、查询缓存仅限查询语句
2.优化器没有用上索引
索引失效
扫描行数太多
主键索引和非主键索引
3.执行器数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。
执行的时候,遇到锁,如表锁、行锁。