数据库系统能够接受 SQL 语句,并返回数据查询的结果,或者对数据库中的数据进行修改,可以说几乎每个程序员都使用过它。所以,解析一下 MySQL 编译并执行 SQL 语句的过程,一方面能帮助你加深对数据库领域的编译技术的理解。另一方面,由于 SQL 是一种最成功的 DSL(特定领域语言),所以理解了 MySQL 编译器的内部运作机制,也能加深你对所有使用数据操作类 DSL 的理解,比如文档数据库的查询语言。另外,解读 SQL与它的运行时的关系,也有助于你在自己的领域成功地使用 DSL 技术。
一条SELECT执行流程
连接层
主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作。如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。
- 账户验证
- 权限分配
- 线程分配
- 如果线程满了,则处于线程等待状态
- 执行结果返回后,该线程不会立刻销毁,而是会将该线程保存起来(线程创建和销毁都牵扯到CPU上下文的切换,会有很大的开销)。如果有别的连接则可以复用该连接,如果长时间没有则会将该线程销毁
Server层
主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog日志模块。
缓存层
- 按照查询语句作为key查找缓存(如果查到直接返回,如果未查到则进入分析器)
分析器
- 词法分析,一条SQL语句有多个字符串组成,首先要提取关键字,比如select,提出查询的表,提出字段名,提出查询条件等等。
- 语法分析,主要就是判断你输入的sql是否正确,是否符合mysql的语法。
- 优化器
- 根据检索条件(索引,表连接 等等),生成执行计划
- 选择成本最低的执行计划,调用执行器
- 执行器
- 执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
小帖士:Mysql8.0为什么删除缓存层
查询语句和查询结果以键值对的形式被直接缓存在内存中。因为是对SQL语句做hash计算,所以查找缓存的时候,SQL语句必须是字节级的匹配,完全一致的SQL才能命中缓存,也意味着在缓存中
SELECT * FROM test
和select * from test
是不一样的。
一旦涉及到缓存,我们就要考虑缓存失效的问题,如果数据表存在数据(表数据,表结构以及索引)的变更,那么这张表的查询缓存就要被清除。这时候频繁的变更必定导致更大的系统开销。此时查询缓存也就要考虑使用的实际场景有哪些了。
最重要的还有一个大招,就是MySQL查询缓存有严重的可伸缩性问题,并且很容易成为严重的瓶颈。它不能与多核计算机上在高吞吐量工作负载情况下进行扩展。关于这个具体说明,可以参考一下MySQL官方人员的博客文章:https://dev.mysql.com/blog-archive/mysql-8-0-retiring-support-for-the-query-cache
引擎层
- 根据查询主键ID绑定的 page_id 找到对应数据页
- 将数据页内容写入到 BufferPool
- 根据二分查找法从 Page Directory 找到对应的数据槽
- 在对应数据槽沿着数据的单向链表查找到对应的数据
- 返回查询结果
一条UPDATE语句的历险记
CREATE TABLE `users` (
`user_id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`sex` char(1) COLLATE utf8mb4_general_ci DEFAULT NULL,
`hobby` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `test`.`users` (`user_id`, `name`, `sex`, `hobby`) VALUES (1, 'tom', '男', '唱歌');
INSERT INTO `test`.`users` (`user_id`, `name`, `sex`, `hobby`) VALUES (2, 'mike', '女', 'rap');
INSERT INTO `test`.`users` (`user_id`, `name`, `sex`, `hobby`) VALUES (3, 'tony', '男', '篮球');
我们根据对 users 表的 name 字段建立的唯一索引,然后执行修改语句
UPDATE `users` SET `hobby` = '跳舞' WHERE `name` = 'make';
然后我们分析一下这个事务的整体执行流程。在介绍 SELECT 的时候大体流程是相同的,所以这里我们只把重要的点找出来分析
Server层
分析器
- 根据二级索引生成对应的执行计划
- 根据主键索引生成对应的执行计划
很明显,如果采用二级扫描只需要回表一次就可以拿到数据。如果采用主键索引方案则需要进行全表扫描。所以第一种方案是最优方案
引擎层
- 根据二级索引B+Tree找到主键ID,然后从主键索引的B+Tree找到数据页的 page_id
- 将数据页写入buffer pool
- 从数据页中找到查询的数据
- 如果找到则判断更新的数据是否发生变化(没有变化则不更新)
- 将要修改的数据写入到 redo-log
- 回滚数据写入到 undo-log
- 事务进入 prepare 状态
Server层
- 执行器
- 判断事务是否处于 prepare 状态
- 将更新记录写入 bin-log
- 将事务提交,事务变成 commit 状态
总结
本文是对InnoDB执行流程一次简单的探索,为了便于读者理解,其中存在一些不完善的地方。例如:
- 分析器对于语句的分析报告
- 优化器如何根据索引计划计算成本
- 索引计划的计算成本因素是什么
- 从InnoDB页中取数据的规则
- 修改数据的时候如果加锁和加在哪里
- 如果是Insert语句,隐式锁升级过程
- 在不同隔离级别下,取出的数据为什么不相同
- MVCC如何在不加锁的情况下保证Mysql的高效查询
是的,Mysql是一个技术栈非常丰富的软件。这里面有很多的疑问和问题,就算拿其中一个写,都需要很长的篇幅去表述。但是不怕,我们把这个庞大的mysql进行拆解,一步步去认识他。带着这些疑问,我们一起来探索Mysql新世界的大门。