前言
在第一篇文章中以MySQL架构篇拉开了MySQL数据库的帷幕,对MySQL的分层架构的每一层进行了详细的解释。
那么在此篇文章中,进一步去深入分析细节,站在一条SQL语句的角度,进行分析,从SQL的诞生开始,直到SQL执行、数据返回等全过程进行分析
一个请求、一个对象、一条SQL三部曲分别对应的是一个网络请求、JVM中的Java是对象从诞生到死亡、SQL语句的诞生到死亡,另外的两篇文章在后续都会学习
接下来对:SQL的诞生,SQL执行前,SQL执行中,SQL执行后分别进行详细的分析
一条SQL是如何诞生的
SQL语句都诞生于客户端 主要有两种方式生成SQL语句:
- 开发者自己手动编写
SQL语句 - 利用相关的
ORM框架进行自动生成,就是我们在学习框架的时候所学的ORM映射关系,一般情况下MySQL运行过程中收到的大部分SQL都是由ORM框架来生成的,例如Java中的MyBatis、Hibernate(感觉这个有点过时了)框架等
那么SQL的生成时机是什么呢? SQL的生成时机一般是和用户的请求有关的,当用户在系统中进行了某个操作,一般都会产生一条SQL,例如我们在浏览器输入某个网址:
select * from juejin_article where id = 7619219125664415784
这条SQL的大致意思就是:根据作者ID从文章表中查询出该作者的所有的文章信息
所以,用户浏览器上看到的数据一般都来自于数据库,而数据库执行的SQL来自于用户的操作,两者是相辅相成的,其他增删改操作也是一样的
总结:本质上,你从浏览器输入网址去访问的操作,都会被转化成一条条对应的SQL
// 请求参数一般是json格式的
// 请求网址,这个是注册的页面
https://www.aaa.com/user/register
// 请求参数
// 一般密码会加密处理,通过工具类进行加密处理
{
user_name: "JunLa",
user_pwd: "123456",
user_sex: "1",
user_phone: "10101010101",
user_address: "aaaa",
......
}
根据上述这个用户注册的案例
当用户进入到该网页点击注册按钮时,就会向该网站的服务器发送一个
post请求,紧接着会根据请求参数,生成一条SQL语句类似于一条
insert操作的SQL语句基本是和
select的流程是一样的,都会根据你在该网站的一个操作,来转换成SQL语句insert into register_table (user_name, user_ pwd, user_sex, user_phone, user_address, ....) values ("JunLa", "123456", "1", "10101010101", "aaaa", ......);
即:一条SQL语句的诞生都来源于一个用户请求(这个请求不仅仅是浏览器输入地址栏这种请求,还有各种请求),在开发程序时,SQL的大体逻辑是根据业务逻辑层的编码决定的,具体的SQL语句是 根据用户的请求参数决定的,以及提前定制好的SQL骨架拼接形成
在Java、go等其他程序中只能生成SQL语句,并不会执行SQL语句,执行SQL语句的工作是由数据库去完成的
一条SQL语句执行前会经历的过程
经过上面的讲述,已经成功生成了一条完整的SQL语句,为了让这条SQL语句能够正常执行,我们首先要去获取一个数据库连接对象因为我们前面讲过SQL语句的执行是通过数据库来执行的,在MySQL架构篇中的网络连接层中维护了一个连接池,但是在客户端,例如Java程序中的druid数据库连接池,那为什么在服务端中有了连接池,我在客户端还需要数据库连接池呢?
在客户端再维护一个数据库连接池的必要性
当要在Java程序中创建一个数据库连接时,首先会读取配置文件中的连接地址、账号密码、用户名等配置信息,然后根据配置的地址信息,发起网络请求获取数据库连接对象,在这个过程中,涉及了网络请求,那此时必然会先经历TCP三次握手的过程,获取连接对象完成SQL操作之后,又要把这个数据库连接对象释放,那么此时就要经历TCP四次挥手过程
很显然,在Java程序中创建数据库连接、关闭数据库连接的过程,消耗资源是很大的,而且当程序上线之后,会频繁对数据库进行操作,如果每次都只能直接去操作数据库,每次都获取新的连接对象,那么就会导致大部分的时间都在做TCP三次握手和TCP四次挥手的工作,系统性能就很低
所以,就需要维护一个数据库连接池,和线程池的思想基本相同,就是利用池化技术对资源进行维护,就是说之后要对数据库进行操作时,不需要自己去建立连接,直接从数据库连接池中获取连接即可,用完之后就释放会数据库连接池,就达到了复用的效果
数据库连接池的连接对象不是一直存在的,如果某个连接对象长时间未进行SQL操作,连接池会自动注销这些连接对象,后面需要使用的时候再次创建,何时创建、何时销毁、连接数限制等这些工作都是交给连接池完成的,开发者无需关注
所以,在客户端再维护一个数据库连接池的目的就是为了提高性能,达到复用的效果
和服务端的连接池都是使用了池化技术,达到了复用的效果,只不过针对的方向不同
客户端的连接池主要是为了实现复用数据库连接的目的,因为每次SQL操作都需要经过TCP三次握手和TCP四次挥手的过程,这个过程同样耗时且占用资源,所以利用池化技术解决了这个问题
MySQL服务端的理解池主要是为了实现复用线程的目的,因为每个数据库连接在MySQL中都会使用一条线程维护,而每次为客户端分配连接对象时,都需要经历创建线程、分配栈空间等繁琐的工作,这个过程需要时间,同时资源的开销也不小,所以利用池化技术来解决
SQL执行前会发送的事情
当完整的SQL生成之后,会先去连接池中尝试获取数据库连接对象,如下图所示的流程:
尝试从数据库连接池中获取连接对象,如果此时连接池中由空闲的连接就可以直接获取进行复用,如果没有就要判断当前池中的连接数是否已经达到最大连接数,如果连接数已经满了,当前线程则需要等到其他线程释放连接对象,没满则可以直接再创建一个新的数据库连接对象
如果此时连接池中没有空闲连接,需要再次创建一个新连接,那么就会发起网络请求建立连接
首先会经过
TCP三次握手的过程后续会详细学习TCP的三次握手过程,当网络连接建立成功之后,就相当于在MySQL中创建了一个客户端会话,然后就进行下面的一系列工作:
-
首先会验证客户端的用户名和密码是否正确
- 如果用户名不存在或密码错误,则抛出
1045的错误码和错误信息 - 如果用户名和密码验证通过,则进入第二部
- 如果用户名不存在或密码错误,则抛出
-
判断
MySQL连接池中是否存在空闲线程- 存在:直接从连接池中分配一条空闲线程维护当前客户端的连接
- 不存在,创建一条新的工作线程(映射内核线程、分配栈空间...)
-
工作线程会先查询
MySQL自身的用户权限表,获取当前登录用户的权限信息并授权
到此,执行SQL前的准备工作就完成了,打通了执行SQL的通道,下一步就是准备执行SQL的语句、工作线程会等待客户端将SQL传递过来
一条SQL语句在数据库中是如何执行的
经过网络连接层的一系列工作后,接着客户端会将要执行的SQL语句通过连接发送过来,然后回进行MySQL系统服务层进行处理,根据用户的不同操作,MySLQ执行SQL语句时也会存在些许差异,这里是指读操作和写操作,两者SQL的执行过程并不相同,下面可以来看看select语句的执行过程
一条查询SQL的执行过程
我们可以通过一个案例来进行演示
select user_id from user_table where user_sex = "男" and user_name = "b"
-- 表数据
+---------+--------------+----------+-------------+
| user_id | user_name | user_sex | user_phone |
+---------+--------------+----------+-------------+
| 1 | a | 女 | 18888888888 |
| 2 | b | 男 | 13588888888 |
| 3 | c | 男 | 15688888888 |
| 4 | d | 女 | 13488888888 |
| 5 | e | 男 | 18588888888 |
| 6 | f | 男 | 17777777777 |
| 7 | g | 女 | 16666666666 |
+---------+--------------+----------+-------------+
先看一个SQL执行的完整流程图,然后进行逐步分析
在客户端和MySQL服务端成功建立连接之后
-
先将
SQL发送给SQL接口,SQL接口回对SQL语句进行哈希处理 -
SQL接口在缓存中根据哈希值检索数据,就是去查询缓存,如果缓存中有数据就直接返回数据 -
缓存中未命中,即缓存中没有数据,就会将
SQL发送给解析器,解析器会对SQL进行判断,看SQL的语句是否正确- 错误:抛出
1064错误码及语法错误信息 - 正确:将
SQL语句发送给优化器进行优化处理
- 错误:抛出
-
优化器根据
SQL制定出不同的执行方案,选择出最优的执行计划 -
工作线程根据执行计划,调用存储引擎所提供的
API获取数据 -
存储引擎根据
API调用方的操作,去磁盘中检索数据(索引、表数据...) -
发生磁盘
IO后,对于磁盘中符合要求的数据逐条返回给SQL接口 -
SQL接口会对所有的结果集进行处理,就是我们常说的处理结果集的操作,(剔除列、合并数据..)并返回数据
上述是对图片的整体流程进行一个简单的解析,一般情况下SQL的查询语句都会经历这些步骤,下面我们研究一下细节过程
SQL接口会做什么工作
当客户端和MySQL建立连接之后,客户端就会向MySQL服务端发送SQL语句,SQL语句紧接着就会被发送给SQL接口进行处理,首先会对SQL做哈希处理,就是根据SQL语句计算出一个哈希值,然后根据这个哈希值去查询缓存,进行比对,如果缓存中存在相同的哈希值,则代表之前缓存过相同的SQL语句的结果,那此时则直接从缓存中获取结果并响应给客户端
如果没有从缓存中查询到数据,接下来就会将SQL语句交给解析器去进行处理
注意,当返回结果数据的时候,SQL接口还需要对结果集进行处理,后续分析
解析器会做什么工作
解析器收到SQL后,会开始检测SQL语法是否正确,也就是词法分析、语义分析等工作,在这一步解析器会根据SQL语言的语法规则,判断客户端传递的SQL语句是否合规,如果不合规就会返回1064错误码及错误信息
ERROR 1064 (42000): You have an error in your SQL syntax; check....
但如果SQL语句没有问题,此时就会对SQL语句进行关键字分析,也就是根据SQL中的select、update、delete等关键字,先判断SQL语句的操作类型,是读操作还是写操作,然后再根据from关键字来确定本次SQL语句要操作的是那张表,也会根据where关键字后面的内容,确定本次SQL的 一些结果筛选条件
总之就是,经过关键字分析后,一条SQL语句要干的具体工作就会被解析出来
解析了SQL语句中的关键字之后,解析器会根据分析出的关键字信息,生成对应的语法树,然后交给优化器进行处理
如果学过JVM的话,这一步相当于就是Java中的
.java源代码变成.class字节码的过程,目的就是将SQL语句翻译成数据库可以看懂的指令
优化器会做什么工作
经过解析器的工作之后得到一个SQL语法树,也就是知道了客户端的SQL大体要干什么事情,接着优化器就会对这条SQL给出一个最优的执行方案,也就是告诉工作线程怎么执行效率最高、最节省资源以及时间
优化器最开始会根据解析器中生成的语法树制定出多个执行计划,然后从这些执行计划中选出一个最优的执行计划,然后把这个执行计划交给工作线程去执行 但是如何在这么多个执行计划中选出最优的执行计划呢? 举个例子:
select user_id from user_table where user_sex = "男" and user_name = "b";
上面这条
SQL语句有多少种执行方案呢?两种:
- 先从表中将所有
user_sex = “男”的数据查询出来,然后再从查询出来的数据中去获取user_name = “b”的数据- 先从表中将
user_name = “b”的数据查询出来,然后再从查询出来的这些数据中获取user_sex = "男"的数据分析:
- 如果使用第一种执行方案,就会先查询到
user_sex = “男”的四条数据,然后再从这四条数据中查询出user_name = “b”的数据- 如果使用第二种执行方案,就会先查询到
user_name = "b"的一条数据,就直接获取到了一条数据,然后再判断一下user_sex = “男”是否成立即可,成立直接返回该条数据,不成立返回空所以很显然就是第二种执行方案最优
- 因为第一种执行方案需要扫描一次全表,然后再对结果集进行逐条判断
- 第二种执行方案扫描一次全表之后只需要再判断一次即可
上述就是解析器的一个工作流程,通过上述案例可以更好地进行理解
但实际的SQL优化过程更复杂,例如多表join查询时怎么查询更合适,单表复杂SQL查询时,有多条索引可以走,走那条更快,一条SQL的最优执行计划要经历多方面的比对和判断来生成
MySQL优化器的一些优化准则:
-
多条件查询时,重排条件先后顺序,将效率更好的字段条件放前面,就例如上面的案例,很明显就是
user_name这个字段的效率更好 -
当表中存在多个索引时,选择效率最高的索引作为本次查询的目标索引
-
使用分页
limit关键字时,查询到对应的数据条数后终止扫表 -
多表
join联查时,对查询表的顺序重新定义,同样以效率为准。 -
对于
SQL中使用函数时,如count()、max()、min()...,根据情况选择最优方案。max()函数:走B+树最右侧的节点查询(大的在右,小的在左)。min()函数:走B+树最左侧的节点查询。count()函数:如果是MyISAM引擎,直接获取引擎统计的总行数。
-
对于
group by分组排序,会先查询所有数据后再统一排序,而不是一开始就排序。
总之:不同的SQL优化器会基于不同的准则选出最优的执行方案
只需牢记:MySQL虽然有优化器,但对效率影响最大的还是SQL语句本身,所以写出一条趋于完美的SQL语句才是提升效率的关键
存储引擎会做什么工作
经过优化器得到了最优的执行方案,接下来工作线程就会根据最优的执行方案去依次调用存储引擎提供的API,前面提过,存储引擎的主要作用就是负责在磁盘上读写数据,不同的存储引擎,存储在本地磁盘中的数据结构不同,但这底层实现并不需要MySQL的上层服务关心,所以上层服务只需要调用存储引擎提供的对应的API即可
工作线程根据执行计划调用存储引擎提供的API查询指定的表,最终会发生磁盘IO,从磁盘中检索出数据,检索的数据有可能时磁盘中的索引文件,也有可能时磁盘中的表数据文件,这点要根据执行计划来决定,我们只需要记住经过这一步之后总能够得到执行结果即可
注意:前面在创建数据库连接的时候,对登录用户进行了授权处理的步骤,当工作线程尝试去查询某张表的时候,首先会去判断一下线程自身维护的客户端连接,这个客户端的登录用户是否有对这张表进行操作的权限,如果不具备就返回权限不足的错误信息
存储引擎从磁盘中检索出目标数据之后,就得返回,那么它是将数据逐条返回给SQL接口,然后通过SQL接口完成最后的数据聚合工作
一条写入SQL的执行过程
写入
SQL和查询SQL是由区别的,即写入操作和读写操作是有差异的对表中的数据进行更改的操作都是写入操作
增删改操作都属于写入操作,我们就以
update操作作为案例演示一下
update user_table set user_sex = "女" where user_id = "b"
下面先给出一个完整的执行流程图:
很明显能看出,相比于查询
SQL要复杂一下,先来整体看一下每一步的流程,然后再进行详细分析:
-
先将
SQL语句发送给SQL接口,SQL接口就会对SQL语句进行哈希处理 -
然后就会在缓存中根据哈希值检索数据,如果缓存中有则将缓存中的对应的表删除,
因为这是写入操作,数据是会发生改变的,为了确保缓存和数据库中的数据的一致性,就得先把缓存中的数据删除掉,然后数据修改完之后再存入缓存 -
如果缓存没有对应的数据,就会将
SQL交给解析器进行处理,解析器就会判断SQL语句是否正确- 错误:抛出
1064错误码及相关的语法错误信息 - 正确:将
SQL语句交给优化器进行处理,进入下一步操作
- 错误:抛出
-
优化器根据
SQL语句制定出不同的执行计划,然后选择出最优的执行计划 -
在执行开始前,先记录一下
undo-log日志和redo-log(prepare状态)日志 -
在缓冲区中查找是否存在当前要操作的行记录或表数据(内存中):
-
存在
- 直接对缓冲区中的数据进行写操作
- 然后利用
checkpoint机制刷新一下,写到磁盘,保证数据的一致性
-
不存在
- 根据执行计划,调用存储引擎对应的
API - 发生磁盘
IO,对磁盘中的数据做写入操作
- 根据执行计划,调用存储引擎对应的
-
-
写入操作完成之后,记录
bin-log日志,同时将redo-log日志中的记录改为commit状态 -
将
SQL执行耗时及操作成功的结果返回给SQL接口,再通过SQL接口返回给客户端
整个写入SQL语句的执行过程,和读操作相比,在缓存的位置有区别,读是尝试从缓存中获取数据,而写操作由于数据会被修改,如果在缓存中发现了要被修改的表,就要把整个表的所有缓存情况,保证数据的一致性
除了上面说的区别,写操作还多了唯一性判断、一个缓冲区的写入,以及几个写入日志,接下来详细看看
- 唯一性判断是针对插入、修改语句的,因为如果表中某个字符建立了唯一约束或唯一索引后,插入/修改数据时,会先检测一下目前插入/修改的值,是否和表中的唯一字段有冲突,如果表中已经存在相同的值,就会抛出异常,反之正常执行
- 由于
CPU和磁盘的性能相差很大,那么MySQL中会在内存中设计一个缓冲区的概念,主要用于弥补CPU和磁盘的性能差距
缓冲区会做什么工作
在真正调用存储引擎API去操作磁盘之前,即在通过优化器获取到最优执行方案之后,先在缓冲区中查找是否有要操作的目标数据/目标表,如果存在则直接对缓存区的数据进行操作即可,那就不需要区调用存储引擎产生磁盘IO了,然后MySQL会在后台通过checkpoint机制来进行数据刷新,确保数据一致性,将缓冲区的更新的数据刷到磁盘,只有当缓冲区没有要被操作的目标数据或目标表的时候才会区真正调用存储引擎的API,然后产生磁盘IO,去对应的磁盘中的表数据进行修改
不管在缓冲区对数据进行操作还是在磁盘对数据进行操作,本质上数据的更改动作都是发生在内存的,就算时修改磁盘数据也是将数据督导内存中进行操作的,然后再将数据写回磁盘中,但是在写SQL执行的前后都会记录日志,这点在写操作日志中细说,这也是写SQL和读SQL的最大区别
写操作的日志
执行读SQL一般不会有状态,即:MySQL执行一条select语句,几乎不会留下什么痕迹,但是有一种特殊情况:慢查询SQL
慢查询
SQL:是一种查询执行过程耗时较长的SQL记录在执行查询
SQL时,大多数的普通查询MySQL是不会关心的,但是慢查询SQL是例外,这类SQL是引起响应缓慢问题的始作俑者,所以一条查询SQL的执行时长超过规定的时间限制就会被记录在案,也就是会记录到慢查询日志中
把慢查询SQL排除在外,其他的查询SQL是没有状态的,但任何一条写SQL都是有状态的,也就是说只要是对数据库造成改变的SQL,在执行的时候都会被记录在日志中。
首先所有的写SQL在执行前都会生成对应的撤销SQL,撤销SQL就是相反的操作,例如你现在执行的是insert,那么生成的撤销SQL就是delete语句,然后会被记录在undo-log撤销/回滚日志,除此之外还会被记录在redo-log日志
redo-log日志是InnoDB引擎专属的,主要是为了保证事务的原子性和持久性,会将写SQL的事务过程记录在案,如果服务器或者MySQL宕机了,重启时可以通过redo-log日志恢复更新的数据,在写SQL正式执行前,会先记录一条prepare状态日志,表示当前写SQL准备执行,然后执行完成并且事务提交之后,这条日志记录的状态才会更改成commit状态
除了上述的
undo-log和redo-log日志外,还会记录bin-log日志,这个日志和redo-log日志很像,都是记录对数据库发生更改的SQL,只不过redo-log是InnoDB引擎专属的而已,bin-log日志是MySQL自带的日志
那内存中的日志数据何时会刷写到磁盘呢?对于这点则是由刷盘策略来决定的,redo-log日志的刷盘策略由innodb_flush_log_at_trx_commit参数控制,而bin-log日志的刷盘策略则可以通过sync_binlog参数控制:
-
innodb_flush_log_at_trx_commit:0:有事务提交的情况下,每间隔一秒时间刷写一次日志到磁盘。1:每次提交事务时,都刷写一次日志到磁盘(性能最差,最安全,默认策略)。2:每当事务提交时,把日志记录放到内核缓冲区,刷写的时机交给OS控制(性能最佳)。
-
sync_binlog:0:同上述innodb_flush_log_at_trx_commit参数的2,交给OS控制(默认)。1:同上述innodb_flush_log_at_trx_commit参数的1,每次提交事务都会刷盘。
一条SQL执行完成后是如何返回的
读SQL和写SQL执行完成之后,由于SQL操作的属性不同,所以两者之间会存在差异
读类型的SQL返回
前面提到过,MySQL执行一条查询SQL语句时,数据是逐条进行返回的,因为等待所有的数据全部查询出来之后,一次性返回一定会才导致内存被撑爆
注意: 这里的返回并不是直接返回给MySQL客户端,而是返回给SQL接口,因为从磁盘中检索出目标数据之后还需要对这些数据进行结果集处理
举个案例:
select user_id from user_table where user_sex = "男" and user_name = "b"
# 那么执行玩这条SQL之后返回的目标数据是
+---------+--------------+----------+-------------+
| user_id | user_name | user_sex | user_phone |
+---------+--------------+----------+-------------+
| 2 | b | 男 | 13588888888 |
# 虽然我的查询语句只要求查询出一个字段,但是检索出来的数据并不是只返回这一个字段的数据,而是返回这一个字段相关的所有数据,把整行数据全部查询出来
然后再从这一行数据中筛选出所需要的结果字段,这个工作是在SQL接口中完成的,还有多表联查时,数据的合并工作也是在SQL接口中完成的
而且就算没有查询到目标数据,也会将执行状态、执行耗时等信息返回给SQL接口,然后由SQL接口向客户端返回NUll
还会顺手把查询到的数据存入缓存
所以是通过SQL接口把数据返回客户端的,下面的执行结果是如何返回给客户端的这个part会详细说
写类型的SQL返回
写类型的SQL在执行完成后,仅会返回执行状态、受影响的行数以及执行耗时,例如
update user_table set user_sex = "女" where user_id = "b";
这条SQL执行完成后会返回Query OK , 1 row affected (0.00 sec) 所以最终返回给客户端的只有受影响的行数,如果写SQL执行成功,这个值一般都是大于0的,因为一般都是有数据被修改了
执行结果如何返回给客户端
执行当前SQL的工作线程,本身也维护着一个数据库连接,这个数据库连接也维护着客户端的网络连接 通过上一篇文章讲的指令show processlist;就能查看
当结果集处理好之后,直接通过Host中记录的地址,将结果集封装成TCP数据报,返回即可
SQL执行总结
SQL执行原理已讲解完毕,只要理解了MySQL的架构,基本上理解SQL执行原理并不会很困难