ToB企服应用市场:ToB评测及商务社交产业平台

标题: 深入浅出MySQL - 架构与执行 [打印本页]

作者: 农民    时间: 2023-6-19 01:31
标题: 深入浅出MySQL - 架构与执行

Photo by Merilin Kirsika Tedder from Pexels
一、MySQL的逻辑架构

MySQL作为一个流行的开源关系型数据库管理系统,它可以运行在多种平台上,支持多种存储引擎,提供了灵活的数据操作和管理功能。MySQL的逻辑架构可以分为三层:连接层服务层引擎层,下方是网上流传度很广的一张架构图。

需要注意的是, 上图描述的是MySQL5.7及以前的逻辑架构,MySQL8.0中正式移除了查询缓存组件, 因为从收集的数据来看查询缓存的命中率很低,即使是在MySQL5.7中查询缓存这个选项也是默认关闭的,所以本篇文章就不对缓存这款内容做解析了。具体可以查看官方的一篇博客:
MySQL :: MySQL 8.0:停用对查询缓存的支持
事实上,如果不去关注其内部的细节,《高性能MySQL》一书中的这张简图也足够让我们对其逻辑架构有一个直观的认知:

1、连接层详解

当客户端发送连接请求时,MySQL服务器会在连接层接收请求,分配一个线程来处理该连接,随后进行身份验证。具体的功能如下:
2、服务层详解

服务层是MySQL中的核心组件,负责提供各种数据库操作所需的基本功能,如SQL语法处理、事务管理、锁管理等。
SQL语法处理

服务层负责从客户端接收来自连接层的SQL查询请求,并进行初始分析、解析和预处理。
事务管理

MySQL的服务层负责事务管理,确保在执行一系列操作时,满足原子性、一致性、隔离性和持久性这四个特性。事务管理涉及的主要功能包括:
缓存管理

MySQL优化器使用缓存来提高查询速度,包括:
3、引擎层详解

引擎层负责存储数据和执行SQL语句。MySQL支持多种存储引擎,每种引擎各有特点,根据实际需求进行选用。当然,只要没有非常明确的特殊需求就不需要更改存储引擎,因为InnoDB在大部分场景下都比其他引擎更加适用。引擎层通过标准API与服务层交互,实现数据的存储和查询。
我们可以在SQL命令行中执行 show engines; 来查看当前支持的存储引擎:
  1. mysql> show engines;
  2. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  3. | Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
  4. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  5. | FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
  6. | MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
  7. | InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
  8. | PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
  9. | MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
  10. | MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
  11. | BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
  12. | CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
  13. | ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
  14. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  15. 9 rows in set (0.00 sec)
复制代码
二、MySQL语句执行流程

SQL语句的执行流程可以简单分为以下几个步骤:
另外请注意,本篇文章依旧是在为后续写MySQL优化流程做知识上的铺垫,所以一些细节会简单介绍,但实际的SQL优化思想会等到后面的文章再详细介绍。
下面我们来详细解释一下SQL语句的执行流程和细节。
1. 语法分析

语法分析是MySQL执行SQL语句的第一步。语法分析器会对SQL语句进行分析,检查其是否符合语法规则。如果SQL语句不符合语法规则,MySQL将会返回一个错误消息。详细的来说又可分为以下几步:
MySQL使用的语法分析器是Bison。它是一种自动生成解析器的工具,可以根据语法规则自动生成语法分析器。下面是一个示例SQL语句:
  1. SELECT name, age FROM student WHERE id = 1;
复制代码
在语法分析阶段,MySQL会进行以下操作:
tokentypevalueSELECTkeywordselectnameidentifiername,symbol,ageidentifierageFROMkeywordfromstudentidentifierstudentWHEREkeywordwhereididentifierid=operator=1number1
  1. select_statement: SELECT select_expression_list FROM table_reference_list [WHERE where_condition]
复制代码
如果以上步骤都没有出现错误,那么MySQL就会认为这条SQL语句在语法分析阶段是正确的,并继续进行后续的处理。否则,MySQL就会报错,并停止执行这条SQL语句。
2. 查询优化

查询优化是MySQL执行SQL语句的第三步。SQL语句在查询优化阶段会经历以下步骤:
举例说明,下面是一个示例SQL语句:
  1. SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
复制代码
则首先在查询重写时,MySQL会将这条SQL语句重写为:
  1. SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80;
复制代码
这样做的好处是:
接下来在查询分解阶段,MySQL会将这条SQL语句分解为两个子查询:
  1. SELECT name, age, id FROM student;
  2. SELECT student_id FROM score WHERE score > 80;
复制代码
预处理时MySQL会对SQL语句进行一些基本的检查和处理,例如检查表名和字段名是否存在,解析参数等。
最后优化器会根据统计信息和成本模型,为SQL语句选择一个最佳的执行计划。
MySQL优化器是负责为SQL语句选择一个最佳的执行计划的模块。执行计划包括了连接顺序,访问方法,索引选择,排序策略等。MySQL优化器是基于成本的优化器(cost-based optimizer),也就是说它会根据统计信息和成本模型来估算不同执行计划的代价,并选择代价最小的那个。
MySQL优化器在选择执行计划时会考虑以下几个方面:
例如,上面能够重写后的SQL语句应该是:
  1. SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80;
复制代码
MySQL优化器在选择执行计划时会进行以下操作:
这么一通分析之后到底有点纸上谈兵,接下来我们在MySQL 8.0 里执行命令,毕竟实践出真知。进入MySQL命令行,利用explain来查看执行计划:
  1. mysql> SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
  2. +----------+------+
  3. | name     | age  |
  4. +----------+------+
  5. | zhangsan |   18 |
  6. | wangwu   |   20 |
  7. +----------+------+
  8. 2 rows in set (0.00 sec)
  9. mysql> explain SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
  10. +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
  11. | id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                                           |
  12. +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
  13. |  1 | SIMPLE      | student | NULL       | ALL  | PRIMARY       | NULL | NULL    | NULL |    4 |   100.00 | NULL                                                            |
  14. |  1 | SIMPLE      | score   | NULL       | ALL  | student_id    | NULL | NULL    | NULL |    8 |    12.50 | Using where; FirstMatch(student); Using join buffer (hash join) |
  15. +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
  16. 2 rows in set, 1 warning (0.01 sec)
复制代码
我们可以在explain的结果中看到,两行的ID值都为1,而我们自己写的SQL语句里有两个Select,说明实际并没有按照我们的原SQL来执行
再使用show warnings来查看实际执行的sql内容:
  1. mysql> show warnings;
  2. +-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  3. | Level | Code | Message                                                                                                                                                                                                                                                              |
  4. +-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  5. | Note  | 1003 | /* select#1 */ select `datasets`.`student`.`name` AS `name`,`datasets`.`student`.`age` AS `age` from `datasets`.`student` semi join (`datasets`.`score`) where ((`datasets`.`score`.`student_id` = `datasets`.`student`.`id`) and (`datasets`.`score`.`score` > 80)) |
  6. +-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  7. 1 row in set (0.00 sec)
复制代码
可以发现,实际执行的时候用的是semi join,这是因为Semi Join返回的结果只包含主表(左表)中满足连接条件的行,而不包含从表(右表)的任何数据。它的主要目的是通过减少要比较的数据量来提高查询性能。通过使用Semi Join,可以避免将两个表的所有数据进行连接,并仅仅关注满足连接条件的部分数据。
但是对于开发人员来说,我们并不需要关注优化器内部的所有决策,因为涉及的因素太多了,所以我们从整体上来看知道大致的优化方向即可。
这里也给出上面示例的建表语句,方便有心的读者自行尝试:
  1. CREATE TABLE student
  2. (
  3.     id   INT PRIMARY KEY auto_increment,
  4.     name VARCHAR(20),
  5.     age  INT
  6. ) charset = utf8mb4;
  7. CREATE TABLE score
  8. (
  9.     student_id INT,
  10.     course     VARCHAR(20),
  11.     score      INT,
  12.     FOREIGN KEY (student_id) REFERENCES student (id)
  13. ) charset = utf8mb4;
  14. INSERT INTO student (id, name, age) VALUES (1, 'zhangsan', 18),(2, 'lisi', 19),(3, 'wangwu', 20),(4, 'zhaoliu', 21);
  15. INSERT INTO score (student_id, course, score) VALUES (1, '数学', 85),(1, '语文', 90),(2, '数学', 75),(2, '语文', 80),(3, '数学', 95),(3, '语文', 100),(4, '数学', 65),(4, '语文', 70);
  16. SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
复制代码
3. 执行SQL语句

执行SQL语句是MySQL执行SQL语句的最后一步。简单来说,执行器会按照执行计划的步骤,逐步执行SQL语句。执行器会根据查询语句,从磁盘读取数据,并将其存储在内存中。然后,执行器会对数据进行排序、分组、聚合等操作,最终生成查询结果。
说的详细一点,一些重要的步骤如下:
实际上,依旧可以通过MySQL命令行来了解其执行过程:
  1. mysql> set profiling = 'ON';
  2. Query OK, 0 rows affected, 1 warning (0.00 sec)
  3. mysql> SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
  4. +----------+------+
  5. | name     | age  |
  6. +----------+------+
  7. | zhangsan |   18 |
  8. | wangwu   |   20 |
  9. +----------+------+
  10. 2 rows in set (0.00 sec)
  11. mysql> show profile for query 1;
  12. +--------------------------------+----------+
  13. | Status                         | Duration |
  14. +--------------------------------+----------+
  15. | starting                       | 0.000094 |
  16. | Executing hook on transaction  | 0.000005 |
  17. | starting                       | 0.000008 |
  18. | checking permissions           | 0.000005 |
  19. | checking permissions           | 0.000004 |
  20. | Opening tables                 | 0.000088 |
  21. | init                           | 0.000009 |
  22. | System lock                    | 0.000009 |
  23. | optimizing                     | 0.000012 |
  24. | statistics                     | 0.000035 |
  25. | preparing                      | 0.000076 |
  26. | executing                      | 0.000065 |
  27. | end                            | 0.000004 |
  28. | query end                      | 0.000005 |
  29. | waiting for handler commit     | 0.000009 |
  30. | closing tables                 | 0.000008 |
  31. | freeing items                  | 0.000021 |
  32. | cleaning up                    | 0.000010 |
  33. +--------------------------------+----------+
  34. 18 rows in set, 1 warning (0.00 sec)
复制代码
可以看到,命令执行结果非常详细的列出了所有步骤,本文只是挑选了一部分来展开说。
具体结合到例子来说明,假设有一条SQL语句如下:
  1. SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80 ORDER BY s.age LIMIT 10;
复制代码
在执行阶段,MySQL会进行以下操作:
三、InnoDB存储结构

InnoDB是MySQL的默认存储引擎,它支持事务、行级锁、外键、MVCC等特性,提供了高性能和高可靠性的数据存储方案。InnoDB的底层结构主要由两部分组成:内存结构和磁盘结构。

图片来源:https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html
内存结构

InnoDB的内存结构主要包括以下几个部分:
如果对这部分内容感兴趣可以看官方文档,这里只做一个简单的介绍。
缓冲池(Buffer Pool)

主要用于缓存表数据和索引数据,加快访问速度。缓冲池是InnoDB内存结构中最重要的部分,通常占用宿主机80%的内存。缓冲池被分成多个页,每页默认大小为16KB,每页可以存放多条记录。缓冲池中的页按照LRU(最近最少使用)算法进行淘汰,同时也被分成两个子链表:New Sublist和Old Sublist,分别存放访问频繁和不频繁的页。

写缓冲(Change Buffer)

主要用于缓存对非聚集索引的修改操作,减少磁盘I/O。写缓冲是缓冲池的一部分,当对非聚集索引进行插入、删除或更新时,不会立即修改磁盘上的索引页,而是先记录在写缓冲中。当缓冲池中的数据页被刷新到磁盘时,会将写缓冲中的修改操作合并到相应的索引页中。

日志缓冲(Log Buffer)

主要用于缓存重做日志(Redo Log),保证事务的持久性。日志缓冲是一个循环使用的内存区域,默认大小为16MB,可以通过参数innodb_log_buffer_size来调整。当事务提交时,会将日志缓冲中的重做日志刷新到磁盘上的重做日志文件中。日志缓冲中的重做日志也会在以下情况下被刷新:日志缓冲已满、每秒钟一次、每个事务检查点一次。
自适应哈希索引(Adaptive Hash Index)

主要用于加速等值查询,提高查询效率。自适应哈希索引是InnoDB根据查询频率和模式自动建立的一种哈希索引,可以将某些B+树索引转换为哈希索引,从而减少树的搜索次数。自适应哈希索引是可选的,可以通过参数innodb_adaptive来开启或关闭。
磁盘结构

表空间(Tablespace)

表空间是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。表空间可以分为以下五种类型¹²³⁴⁵:
数据字典(Data Dictionary)

数据字典包含用于跟踪对象,如表,索引,和列等元数据的内部系统表²⁴。元数据实际上位于InnoDB系统表空间中。InnoDB使用数据字典来管理和访问数据库对象,并检查用户对对象的权限。数据字典在数据库启动时加载到内存中,并在数据库关闭时刷新到磁盘上。
双写缓冲区(Doublewrite Buffer)

双写缓冲区位于系统表空间中的存储区域,用于保证数据页在写入磁盘时不会损坏²⁴⁵。InnoDB在Buffer Pool中刷新页面时,会将数据页写入doublewrite缓冲区后才会写入磁盘。如果在写入OS Cache或者磁盘mysql进程奔溃后, InnoDB启动崩溃恢复能从doublewrite找到完整的副本用来恢复。
重做日志(Redo Log)

重做日志是基于磁盘的数据结构,在崩溃恢复期间用于纠正不完整事务写入的数据  。MySQL以循环方式写入重做日志文件,默认会产生ib_logfile0 和 ib_logfile1两个文件。InnoDB在提交事务之前刷新事务的redo log,InnoDB使用组提交(group commit)技术来提高性能。重做日志记录了数据页的物理修改,而不是逻辑修改,这样可以减少日志的大小和恢复的时间。重做日志可以通过innodb_log_file_size和innodb_log_files_in_group参数来调整大小和数量。
更改缓冲区(Change Buffer)

更改缓冲区是Buffer Pool中的一部分,用于缓存对辅助索引页的修改  。当InnoDB需要修改一个辅助索引页时,如果该页在Buffer Pool中,则直接修改;如果该页不在Buffer Pool中,则将修改记录在Change Buffer中,而不是从磁盘读取该页。这样可以减少磁盘I/O操作,提高性能。Change Buffer中的修改会在后台或者检查点时合并到辅助索引页中。Change Buffer的大小可以通过innodb_change_buffer_max_size参数来调整。
四、InnoDB磁盘空间管理结构

这部分简单介绍即可,参考官方文档:MySQL :: MySQL 8.0 参考手册 :: 15.11.2 文件空间管理
InnoDB的磁盘结构主要包括以下几个部分:

参考资料:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4