SQL优化和执行

一条SQL语句,在MySQL中是如何执行的

select * from T where ID=10;
在这里插入图片描述
update tb_stu A set A.age=‘19’ where A.name=‘张三’;

update语句的执行过程和select语句差不多,但是在update语句执行的过程中,MySQL新增加了两个重要的日志模块,他们分别是redo log(重做日志)和binlog(二进制日志、也可以称之为归档日志)。

Innodb存储引擎中,内存和磁盘是通过数据页进行交换的,而内存的处理速度远远超过磁盘处理速度,当我们update操作的时候,需要访问数据页,如果每次访问数据页的时候,都和磁盘进行交互,那么无疑是非常耗时的。redo log的出现就是为了解决这个问题,redo log既存在于内存中,又存在于磁盘中,当我们update一条记录的时候,Innodb会先把记录写在redo log中,然后告诉客户端更新完毕了,但其实redo log并没有落在磁盘上,落磁盘的动作是由MySQL在空闲时候处理的,这样能够最大程度上保证MySQL的性能。

redo log和binlog的三点不同之处:

1、redo log是innodb存储引擎层面特有的,binlog是Server层面的,任何引擎都能用!!!

2、redo log是物理日志,记录的是在这个页面上做了什么修改,例如把比特位从0改为1;binlog是逻辑日志,记录的是对某个字段的变更,例如给字段id加1。

3、redo log是循环写的,空间固定;binlog是追加写的,写满之后会切换到下一个,不会覆盖

update操作究竟做了什么?
当我们执行一个update的SQL时,MySQL会干如下几件事情:

a、执行器查找指定记录,如果记录所在的数据页在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

b、执行器拿到Innodb存储引擎接口给的数据,执行update操作,得到新的数据,然后调用Innodb存储引擎的接口写入数据。

c、innodb存储引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。

d、执行器生成update操作的binlog,并把binlog写入磁盘。

e、执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

在这个过程中,MySQL server端的执行器和innodb存储引擎频繁进行交互,画成流程图就是:
在这里插入图片描述
其中,涂蓝色的是在Server层面执行的,白色框代表在Innodb层面执行的。

这里,我们注意最后3个步骤,其中,redo log的写入分为2个步骤,第一个步骤时prepare阶段,第二个步骤时commit,这就是我们常说的redo log"两阶段提交".

两阶段提交是如何保证数据一致的?

我们知道,redo log结合binlog,可以保证在实例宕机或者误操作的情况下恢复出来的数据一致,也可以让我们将数据库恢复到历史的"任意一秒"。本质上,这个能力也是基于"两阶段提交"的。那么,接下来的过程,就是如何保证redo log恢复出来的结果和binlog恢复出来的结果一致了。

先来看看如果不用"两阶段提交"的方法,会发生什么问题。在没有"两阶段提交"的情况下,如果我们要将id=0改为id=1,那么MySQL只有两种情况,先写binlog 再写redo,或者先写redo再写binlog,分别来看:

1、先写binlog再写redo,binlog完成之后,服务器crash了,binlog中id=1了,但是由于redo log没有写,在服务器恢复的时候,恢复出来的值是0,但是我们用binlog恢复出来的值将会变成1,二者产生了不一致的现象;

2、先写redo log,再写binlog,假设redo log写完之后MySQL崩溃重启,仍然能够把数据恢复回来,所以恢复后这一行c的值是1,但是由于没有写入binlog, 那么使用binlog恢复出来的值就是0,和实际结果不符。

一般情况下,当我们出现误操作时,大概率会使用全量备份+binlog的方式恢复数据,而如果此时使用binlog恢复出来的数据有误,那无疑会对业务产生影响。

有了"两阶段提交",我们把整个过程拆分为三个部分:

  1. prepare阶段
  2. 写binlog
  3. commit

当实例宕机后,恢复的过程如下:

情况1:当在2之前崩溃时
重启恢复:后发现没有commit,回滚。
备份恢复:没有binlog 。
重启恢复和备份恢复一致

情况2:当在3之前崩溃
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。
备份恢复:有binlog.
重启恢复和备份恢复一致

关联查询

EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.f1=t2.f1);

在这里插入图片描述
在这条语句里,被驱动表t2的字段f1上有索引,join过程用上了这个索引,因此这个语句的执行流程是这样的:

  1. 从表t1中读入一行数据 R;
  2. 从数据行R中,取出f1字段到表t2里去查找;
  3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
  4. 重复执行步骤1到3,直到表t1的末尾循环结束。

在这个流程里:

  1. 对驱动表t1做了全表扫描,这个过程需要扫描160行;
  2. 而对于每一行R,根据f1字段去表t2查找,走的是树搜索过程。由于两个表的数据都是不是一一对应的,因此每次的搜索过程要扫描26行,也是总共扫描160*26行;
  3. 所以,整个执行流程,总扫描行数是160+160*26。

BNL使用对在外部循环中读取的行进行缓冲,以减少必须读取内部循环表的次数
EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.f1=t2.f1) WHERE t1.f1 = 2;

在这里插入图片描述
在这个语句中,被驱动表t2的字段f1上有索引,join过程没有用上这个索引因而使用上了join buffer,因此这个语句的执行流程是这样的:

  1. 将表t1,t2的已用列读入join buffer中;
  2. 由于join_buffer是以无序数组的方式组织的,因此对表t2中的每一行,都要做160次判断,总共需要在内存中做的判断次数是:160 * 160次。

在这个流程里:

  1. 对驱动表t1做了全表扫描,这个过程需要扫描160行;
  2. 对被驱动表t2做了全表扫描,这个过程需要扫描160行。

这个使用可以总计一个计算公式:如果驱动表的行数为N,被驱动表的行数为M,驱动表走全表扫描,被驱动表走的是索引数的查找,那么驱动表在查找一条数据后,在被驱动表上走普通索引a,在根据普通索引上的主键回表查询数据,走一次树搜索的时间复杂度为log2M,回表一次就是2log2M,驱动表全表扫描,扫描行数为N,那么总扫描行数为N+N2*log2M,那么可以总结出,驱动表的数据越小,整个过程扫描的行数就越小,因此我们应该使用小表作为驱动表,大表作为被驱动表。
//https://blog.csdn.net/huxiaodong1994/article/details/91668304

猜你喜欢

转载自blog.csdn.net/m0_46269902/article/details/112185261