MySQL内部组件
Server层
主要包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
连接器
我们知道由于MySQL是开源的,他有非常多种类的客户端:navicat,mysql front,jdbc,SQLyog等非常丰富的客户端,这些客户端要向mysql发起通信都必须先跟Server端建立通信连接,而建立连接的工作就是有连接器完成的。
词法分析器
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。
如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 from 写成了
“rom”。
mysql> select * fro test where id=1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'fro test where id=1' at line 1
词法分析器
词法分析器分成6个主要步骤完成对sql语句的分析
- 词法分析
- 语法分析
- 语义分析
- 构造执行树
- 生成执行计划
- 计划的执行
下图是SQL词法分析的过程步骤:
SQL语句的分析分为词法分析与语法分析,mysql的词法分析由MySQLLex[MySQL自己实现的]完成,语法分析由Bison生成。关于语法树大家如果想要深入研究可以参考这篇wiki文章:https://en.wikipedia.org/wiki/LR_parser。那么除了Bison外,Java当中也有开源的词法结构分析工具例如Antlr4,ANTLR从语法生成一个解析器,可以构建和遍历解析树。经过bison语法分析之后,会生成一个这样的语法树。
优化器
经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的语句,这个语句是执行两个表的 join:
mysql> select * from test1 join test2 using(ID) where test1.name=yangguo and test2.name=xiaol
ongnv;
既可以先从表 test1 里面取出 name=yangguo的记录的 ID 值,再根据 ID 值关联到表 test2,再判断 test2 里面 name的值是否等于 yangguo。
也可以先从表 test2 里面取出 name=xiaolongnv 的记录的 ID 值,再根据 ID 值关联到 test1,再判断 test1 里面 name的值是否等于 yangguo。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错等等。
执行器
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:
- 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。你会在数据库的慢查询日志中看到一个rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的。
缓存(8中被移除)
连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
大多数情况查询缓存就是个鸡肋,为什么呢?
因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。
因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率
会非常低。
Store层(引擎层)
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说如果我们在create table时不指定表的存储引擎类型,默认会给你设置存储引擎为InnoDB。
索引数据结构
- 二叉树
- 红黑树
- Hash
- B-Tree
二叉树
可能会形成链表,对查询效率提升有限
红黑树
虽然会自动调整,但是树高度不可控,最终也会导致多次磁盘IO,对大数据量支持不好
Hash
- 对索引的key进行一次hash计算就可以定位出数据存储的位置
- 很多时候Hash索引要比B+数索引更高效
- 仅能满足“=”,“IN”,不支持范围查找
- hash冲突问题
B-Tree与B+Tree
- B+是B的变种,B能解决的问题,B+也能解决
- B+的扫库能力更强,B需要扫描整棵树,而B+只需要遍历叶子节点
- 由于根节点及支节点不保存数据,因此读写能力更强(同样树高度下存储数据更多)
- B+叶子结点有指针相连接,因此排序能力更强
- 稳定,但是Btree可能效率更高(根节点命中直接返回)
InnoDB
聚集索引(聚簇)
- 聚集索引:类似于InnDB主键,索引与数据保存在同一文件中(.idb),查询速度更快,普通索引不是聚集索引
- 非聚集索引:类似MyISAM索引,索引(.MYI)和数据(.MYD)在不同文件中,主键和普通索引结构一致
InnoDB为什么一定要主键?
- InnDB是聚簇索引,会以B+tree的方式组织数据,此时需要一个主键字段
- 如果没有主键字段,mysql会选取一列数据都不同的字段形成树
- 如果没有选取中,则会生成隐藏列(rowId)形成树
主键为什么要整型自增?
- 查询效率高
- 占用空间小
- 主键自增会提高插入效率(总是往末尾插入数据)
为什么非主键索引结构叶子结点只存放主键
- 一致性
- 节省空间
为什么有最左前缀原则
这个问题要从联合索引数据结构回答,当构成联合索引时,只有最左列是排好序的,不带最左列只用其他列进行查询时,都会发生全表扫描
Explain
id列
id列是select的序列号,id越大越先执行
select_type
表示对应行是简单还是复杂查询
- simple
简单查询,不包含子查询和union - primary
复杂查询中最外层select - subquery
子查询 - derived
派生查询(包含在from中) - union
在union中的第二个和随后的select
table列
访问的表,假如是格式,则N代表id
type列
从最优到最差分别为:system > const > eq_reg > ref > range > index > ALL
- NULL
通常是min,max这类函数,优化阶段直接返回 - system
const的一个特例,查询表中只有一条元素匹配 - const
对查询的优化可以将其转化成一个常量(用主键或唯一索引查询) - eq_ref
使用主键货unique key索引外连接 - ref
使用普通索引查询或关联查询 - range
范围查询 - index
扫描二级索引不进行回表即可获取所有元素,一般为使用覆盖索引
(即二级索引上有我们需要查询的所有列数据) - ALL
全表扫描(扫描聚簇索引的所有叶子节点)
possible_keys
查询可能会使用哪些索引来查找,当然也会不使用该key(索引帮助不大,直接全表查询)
key
实际采用的索引
key_len
使用索引字节数(通常判断用了联合索引覆盖情况)
计算规则如下:
- 字符串
- char(n):汉字3n
- varchar(n):utf-8下汉字3n+2(大于255,小于+1)
- varchar(n):utf-8mb4下汉字4n+2
- 数值类型
- tinyint:1字节
- smallint:2字节
- int:4字节
- bigint:8字节
- 时间类型
- date:3字节
- timestamp:4字节
- datetime:8字节
注意点:
如果允许为NULL,则会多一个字节判断是否为NULL
索引最大长度为768字节,过长会左前缀处理
ref
查询所用到的列嚯常量
rows
估计要读取并检测的行数,不是结果集行数
Extra列
- Using index(覆盖索引)
- Using where(使用where)
- Using index condition(不完全被索引覆盖)
- Using temporary(创建临时表)
- Using filesort(使用外部排序)
- Select tables optimized away(使用聚合函数,效率高)