一条sql查询、更新语句是如何执行的

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_41594698/article/details/102094984

来源:
《MySql实战45讲》
https://www.bilibili.com/video/av49181542?from=search&seid=8661669117645942366
https://yq.aliyun.com/articles/689760

1 一条sql查询语句是如何执行的

MySQL逻辑架构图:
在这里插入图片描述
实际架构图:
在这里插入图片描述

1.1 第一步:连接器

首先要连接到数据库上,需要权限认证

⼀个⽤户成功建⽴连接后,即使你⽤管理员账号对这个⽤户的权限做了修改,也不会影响已经存在连接的权限;
修改完成后,只有再新建的连接才会使⽤新的权限设置。

连接分为长连接和短连接,一般建议使用长连接,但是长连接有一个问题:有些时候MySQL占⽤内存涨得特别快,这是因为MySQL在执⾏过程中临时使⽤的内存是管理在连接对象⾥⾯的。这些资源会在连接断开的时候才释放。所以如果⻓连接累积下来,可能导致内存占⽤太⼤,被系统强⾏杀掉(OOM),从现象看就是MySQL异常重启了;
解决方案:

  1. 定期断开⻓连接。使⽤⼀段时间,或者程序⾥⾯判断执⾏过⼀个占⽤内存的⼤查询后,断开连接,之后要查询再重连。
  2. 如果使用MySQL 5.7或更新版本,可以在每次执⾏⼀个⽐较⼤的操作后,通过执⾏ mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

1.2 第二步:查询缓存

查询缓存是个鸡肋,⼤多数情况下建议不要使⽤查询缓存,为什么呢?
因为查询缓存往往弊⼤于利。查询缓存的失效⾮常频繁,只要有对⼀个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你把结果存起来后,还没使⽤就被⼀个更新全清空了。
对于更新压⼒⼤的数据库来说,查询缓存的命中率会⾮常低。
除⾮业务就是有⼀张静态表,很⻓时间才会更新⼀次。⽐如,⼀个系统配置表,那这张表上的查询才适合使⽤查询缓存。

8.0开始删去了缓存功能。

1.3 分析器

两步:词法分析、语法分析

词法分析:使用者输⼊的是由多个字符串和空格组成的⼀条SQL语句,MySQL需要识别出⾥⾯的字符串分别是什
么,代表什么。

语法分析:根据词法分析的结果,语法分析器会根据语法规则,判断输⼊的这个SQL语句是否满⾜MySQL语法。

如果表中没有字段k,⽽执⾏了语句 select * from test where k=1, 会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。
这个错误就是在此阶段报出来的,此阶段会判断语句是否正确,表是否存在,列是否存在等

1.4 优化器

1 决定使用哪个索引

2 使用join时,决定各个表的连接顺序

1.5 执行器

1 判断⼀下使用者对sql语句的表有没有执⾏查询的权限

2 根据表的引擎定义,去使⽤这个引擎提供的接⼝,执行sql语句

如select * from test where id = 1的执行流程为:

1 调⽤InnoDB引擎接⼝取这个表的第⼀⾏,判断id值是不是1,如果不是则跳过,如果是则将这⾏存在结果集中;

2 调⽤引擎接⼝取“下⼀⾏”,重复相同的判断逻辑,直到取到这个表的最后⼀⾏。

3 执⾏器将上述遍历过程中所有满⾜条件的⾏组成的记录集作为结果集返回给客户端。
如果id字段有进行索引,那么第⼀次调⽤的是“取满⾜条件的第⼀⾏”这个接⼝,之后循环取“满⾜条件的下⼀⾏”这
个接⼝,这些接⼝都是引擎中已经定义好的。

数据库的慢查询⽇志中看到⼀个rows_examined的字段,表示这个语句执⾏过程中扫描了多少⾏;
这个值就是在执⾏器每次调⽤引擎获取数据⾏的时候累加的。

2 ⼀条SQL更新语句是如何执行的

更新语句涉及到redolog和binlog两个日志

2.1 redolog

这是InnoDB独有的日志

在MySQL里,如果每⼀次的更新操作都需要写进磁盘,那么磁盘要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很⾼。

为了解决这个问题,MySQL使用了WAL技术(Write-Ahead Logging):先写日志,再写磁盘

具体来说,当有⼀条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了;
同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘⾥⾯,⽽这个更新往往是在系统⽐较空闲的时候做,这样效率就大大提升了。

InnoDB的redo log是固定⼤⼩的,比如可以配置为⼀组4个⽂件,每个文件的大小是1GB,那么日志总共就可以记录4GB的操作。
从头开始写,写到末尾就⼜回到开头循环写,如下图:
在这里插入图片描述
write pos是当前记录的位置,一边写入一边右移,到了文件3的末尾就重新移动到文件0的开头继续写;
checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据⽂件。

write pos和checkpoint之间的是空着的部分,可以⽤来记录新的操作。
“空着的部分”指的是write pos 到3号⽂件末尾,再加上0号⽂件开头到checkpoint 的部分。

如果write pos追上checkpoint,表示写满了,这时候不能再执⾏新的更新,需要停下来先擦掉⼀些记录,把checkpoint推进⼀下才能继续写。

有了redo log,InnoDB就可以保证即使数据库发⽣异常重启,之前提交的记录都不会丢失,这个能⼒称为crash-safe。

2.2 binlog

Server层也有⾃⼰的日志,称为binlog(归档⽇志),redolog是循环写的,不持久保存,binlog的“归档”这个功能,redolog是不具备的。

binlog有两种模式:statement 格式是记sql语句, row格式会记录⾏的内容,记两条,更新前和更新后都有。

为什么有两份日志?

最开始MySQL⾥并没有InnoDB引擎。
MySQL⾃带的引擎是MyISAM,但是MyISAM没有crash-safe的能⼒,binlog⽇志只能⽤于归档。
⽽InnoDB是另⼀个公司以插件形式引⼊MySQL的,既然只依靠binlog是没有crash-safe能⼒的,所以InnoDB使⽤另外⼀套⽇志系统——也就是redo log来实现crash-safe能⼒。

区别:

1 redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使⽤。

2 redo log是物理⽇志,记录的是“在某个数据⻚上做了什么修改”;
binlog是逻辑⽇志,记录的是这个语句的原始逻辑,比如“给ID=2这⼀⾏的c字段加1 ”。

3 redo log是循环写的,空间固定会⽤完;
binlog是可以追加写⼊的。“追加写”是指binlog⽂件写到⼀定⼤⼩后会切换到下⼀个,并不会覆盖以前的⽇志。

2.3 更新过程

sql为:

create table T(ID int primary key, c int);
update T set c=c+1 where ID=2; # 更新
  1. 执行器先找引擎取ID=2这⼀行。ID是主键,引擎直接用树搜索找到这⼀行。如果ID=2这⼀行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读⼊内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的⼀行数据,再调用引擎接口写⼊这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log⾥⾯,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的binlog,并把binlog写⼊磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写⼊的redo log改成提交(commit)状态,更新完成。

流程图如下,图中浅⾊框表示是在InnoDB内部执⾏的,深⾊框表示是在执⾏器中执⾏的:
在这里插入图片描述
最后三步将redo log的写⼊拆成了两个步骤:prepare和commit,这就是"两阶段提交",保证了两份日志之间的逻辑一致

如果不使用两阶段提交,而是先写完一个日志再写完另一个,会产生如下问题:

  1. 先写redo log后写binlog的情况:假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。
    redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这⼀⾏c的值是1。
    但是由于binlog没写完就crash了,这时候binlog⾥⾯就没有记录这个语句。因此,之后备份⽇志的时候,存起来的binlog⾥⾯就没有这条语句。
    如果需要⽤这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这⼀次更
    新,恢复出来的这⼀⾏c的值就是0,与原库的值不同。
  2. 先写binlog后写redo log的情况:如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务⽆效,所以这⼀⾏c的值是0。但是binlog⾥⾯已经记录了“把c从0改成1”这个⽇志。所以,在之后⽤binlog来恢复的时候就多了⼀个事务出来,恢复出来的这⼀⾏c的值就是1,与原库的值不同。

使用两阶段提交,可以实现如“让数据库恢复到半个⽉内任意⼀秒的状态”这样的需求,因为binlog的逻辑与原库一致:找到最近的⼀次全量备份,从这个备份恢复到临时库,然后从备份的时间点开始,将备份的binlog依次取出来,重放到需要的状态的那个时刻即可。

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/102094984