一条SQL语句是如何执行的?

SQL语句执行的经过

从用户发起请求,到服务接口调用MySQL驱动,MySQL服务器执行完SQL语句返回结果中间发生了什么?首先放一张图来看整个过程使用到的各个组件,然后再对各个过程进行分析。

SQL语句执行链路

1. 连接过程

以Openresty服务器为例,Openresty是多进程+I/O多路复用结构(Nginx的I/O模型),可以支撑高的并发,一个Worker就是一个进程,一个进程可以处理多条请求。
我们知道当需要执行SQL语句时需要先于MySQL服务器建立连接,如果每个一个请求都建立一个连接,使用完再关闭连接,如果频繁的创建和销毁连接显然是不合理的,浪费系统资源造成性能下降,这时连接池就出现了。

连接池

连接池会维护多个(长)连接,一个SQL语句执行时分配一个连接,使用完不会销毁连接,而是放到空闲队列中等待下次使用,这样可以在高并发的场景大大减少创建、销毁连接带来的性能问题。

连接器

类似Web服务器通过连接池维护与数据库服务器的连接,MySQL的连接器提供了同样的功能,也维护了一个连接池,不同的是MySQL连接器同时还有权限验证的功能。

  • 修改密码不会影响已经建立的链接。
  • 连接完成后如果没有操作,改连接就会处于空闲状态,可以使用show processlist命令查看,如果长时间没有操作连接器会在到达超时时间后断开它。

长连接

长连接是客户端持续有请求,使用的是同一个连接,建立连接的过程通常是比较慢的,建议尽量使用长连接。但是长连接累计较多时可能会导致内存过大(内存管理在连接对象里),比系统强行kill,引起MySQL异常重启,可以使用以下两种方法解决:

  • 定期断开长连接。
  • 如果是MySQL5.7及以上的版本,可以在每次执行一个比较大的操作后执行mysql_reset_connection重新初始化连接资源(不会重新建立连接)。

2. 执行过程

查询缓存

如果是查询语句,而且开启了查询缓存,连接器拿到一个查询请求后,会先查看查询缓存是否有(之前执行过这条语句)。缓存key为sql语句,value是查询结果。

  • 不建议开启查询缓存,除非是基本不会变的数据表。因为只要对表有更新,该表上的所有查询缓存都会清空,导致查询命中率很低。

分析器

分析器的功能就是对SQL语句做词法分析和语法分析,解析这条语句要干什么,语法错误会返回错误提醒。

优化器

优化器是在表中有多个索引的时候决定使用哪个索引,或者有多表关联(join)的时候决定各个表的连接顺序。

执行器

通过分析器知道了要做什么,优化器知道了改怎么做,执行器就是真正的语句执行阶段。开始执行的时候要先判断对表是否有权限(在优化器之前也会做预检查)。执行器会调用存储引擎提供的接口进行读写操作。

3. 更新语句执行过程

查询语句是只读的,比较简单,经过一系列组件最终查询到结果返回。但是更新语句就相对复杂一些,涉及到两个日志模块:redo log和binlog。

redo log(重做日志)

如果每次更新都要刷盘,整个过程磁盘IO成本、查询成本都比较高,为了提升更新效率,InnoDB引擎提供了redo log(顺序写入速度很快)。

WAL(Write-Ahead Logging):先写日志,再写磁盘。当有一条记录需要更新的时候,InnoDB引擎会先将记录写入redo log并更新内存,这时候更新就算完成了,再需要的时候再将这个操作更新到磁盘里。

日志结构:redo log大小是固定的,比如配置一组4个文件,每个文件1G,
就可以记录总共4G的记录。从头开始写,写完后又回到开头循环写入。

crash-safe:故障安全,redo log除了能提高更新操作的效率,同时还保证了故障安全,在数据库异常时不会导致数据丢失。

binlog(归档日志)

MySQL最开始没有InnoDB引擎,binlog日志只用于归档和复制,只依靠binlog没有crash-safe能力。

  • redo log是InnoDB引擎独有的,属于存储层;binlog是MySQL提供的,属于server层
  • redo log是物理日志,记录在某个数据页上做了什么修改;binlog是逻辑日志,记录SQL语句的原始逻辑
  • redo log是循环写的,空间固定,用完会从头开始写;binlog是追加写的,一定大小后切换到下一个文件,不会覆盖

Buffer Pool缓冲池

InnoDB重要的内存结构,数据的操作都是在Buffer Pool中操作的,如果数据不在缓冲内存中,会先从磁盘中读取到数据页到缓冲池,然后再执行相关操作。

update执行过程

update T set k = k + 2 where id = '1' limit 1

  1. 执行器调引擎读接口找id=2这一行,如果数据页本来在内存就直接返回,否则先从磁盘load到内存中再返回。
  2. 然后执行器将k值加上2,得到新的一行数据,在调用引擎的写接口写入这行新数据。
  3. 引擎将这行数据更新到内存中,同时将更新操作记录到redo log,此时redo log处于prepare状态,然后告知执行器执行完成,可以提交事务。
  4. 执行器生成这个操作的binlog并写入磁盘。
  5. 执行器调用存储引擎的事务提交接口,引擎把刚写入redo log改为commit状态,更新完成。

两阶段提交

保证了crash-safe能力,如果不使用两阶段提交,使用binlog恢复数据库时会导致与原数据库状态不一致。

假如不使用两阶段提交,在写日志时机器发生故障:

  1. redo log写入(比如k,本来为0,执行更新后
    k = 2)后发生故障,binlog未写入。由于redo log写完之后即使系统崩溃,也会能将数据恢复,恢复后这一行数据k=2。但是binlog没写完就crash,binlog没有记录这条语句,如果使用binlog来恢复时会少一个事务,恢复后的k=0,原数据库k=2。
  2. binlog写入后发生故障,redo log未写入。redo log为写入,崩溃后这个事务无效,k=0。但是binlog已经记录了更新语句,之后恢复时会多出一个事务,恢复后k=2,原数据库k=0。

总结

  1. MySQL连接器使用连接池维护连接,并进行检查权限,接收一个SQL语句
  2. 然后通过分析器、优化器知道如何执行SQL语句
  3. 通过执行器与存储引擎交互,完成数据的读写。
  4. 数据更新同时会写入两个重要的日志文件:redo log和binlog,并通过两阶段提交保证了crash-safe能力。

参考资料
【码农有道】详解一条 SQL 的执行过程
【极客时间】MySQL实战45讲01、02讲

你可能感兴趣的:(一条SQL语句是如何执行的?)