MySQL 的架构共分为两层:Server 层和存储引擎层
Server层
Server 层主要负责建立连接、分析和执行 SQL。MySQL 里大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数和所有跨存储引擎的功能比如存储过程、触发器等都在 Server 层实现。
存储引擎层
存储引擎层主要负责数据的存储和提取。MySQL支持多个存储引擎,不同的存储引擎共用一个 Server 层。从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。
第一步:通过连接器进行连接
我们首先需要连接 MySQL 服务器,然后才能执行 SQL。因为 MySQL 是基于 TCP 协议进行传输的,所以连接的过程需要先经过 TCP 的三次握手。
第二步:查询缓存
如果 SQL 是查询语句,MySQL 就会先去缓存里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。
如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。
但其实对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,那这样也太浪费资源了。所以,MySQL 8.0 版本直接将查询缓存删掉了。
第三步:解析器解析 SQL
在正式执行 SQL 查询语句之前, MySQL 会先对 SQL 语句做解析,这个工作交由「解析器」来完成。
解析器会做如下两件事情。
1,词法分析。MySQL 会根据你输入的字符串识别出关键字,比如表名、字段名、where条件这些,并且构建出 SQL 语法树。
2,语法分析。也就是检验我们输入的 SQL 语句语法是否正确,比如把 from 写成了 form,这时 MySQL 解析器就会报错。
第四步:执行SQL
SELECT 查询语句流程主要可以分为三个阶段:
1、预处理阶段;
检查 SQL 查询语句中的表或者字段是否存在;把 select* 中的 * 符号,扩展为表上的所有列;
2、优化阶段;
优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。
3、执行阶段;
根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
默认是在 /var/lib/mysql/
目录里,一个xx数据库目录下有三个文件
db.opt, 用来存储当前数据库的默认字符集和字符校验规则。
xx.frm,用来存储表结构,每建立一张表都会生成一个.frm 文件。
xx.ibd,用来存储表数据,每一张表的数据都存放在一个独立的 .ibd 文件。
表空间由行、页、区、段组成。
1、行
数据库表中的记录都是按行进行存放的,每行记录根据不同的行格式,有不同的存储结构。
2、页
虽然记录是按照行来存储的,但是数据库的读取并不以行为单位,因为如果每次只能处理一行数据,效率会非常低。所以,InnoDB 的数据是以「页」为单位来读写的。每当需要读一条记录的时候,会把包含这条记录的整个页从磁盘读入内存。
页是 InnoDB 存储引擎磁盘管理的最小单元,它默认的大小是16KB,所以数据库每次读写都是以 16KB 为单位的。
3、区
4、段
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。
InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。
1、Redundant
Redundant是MySQL5.0之前使用的格式,现在已经废弃了。
2、Compact
这个比较经典,后面的Dynamic和Compressed 都是基于Compact改造而来的。
在compact行格式下,一行记录分为两个部分:额外信息和真实数据。
额外信息
额外信息包含 3 个部分:变长字段的长度列表、NULL 值列表、记录头信息
变长字段的长度列表主要是存储变长字段所占用的数据大小,然后读取的时候可以根据这个长度列表来读取对应的数据长度。它在存储时是按照列的逆序来存放的。之所以要逆序,是因为记录头信息中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
如果表里没有变长字段,那行格式就不会有变长字段长度列表。
NULL 值列表:因为表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。如果列是NULL,那就给它对应值为1的二进制位。如果列值不是NULL,就给它一个0的二进制位。存放时也是逆序存放。
当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。
记录头信息:包含比较多的东西,比如delete_mask 标识这条数据是否被删除。next_record下一条记录的位置等等。
真实数据
记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer。
如果表里既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段,占六个字节。
trx_id是事务id,表示这个数据是由哪个事务生成的,占6个字节。
roll_pointer,这条记录上一个版本的指针。roll_pointer 是必需的,占 7 个字节。
Dynamic
Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。
Compact 在行溢出时,会在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
而Compressed 和 Dynamic 在行溢出时,记录的真实数据处只存储 20 个字节的指针来指向溢出页,而实际的数据全都存储在溢出页中。
索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说索引就相当于数据的目录。
InnoDB 存储引擎会根据不同的场景选择不同的列作为索引,默认底层使用的数据结构就是B+Tree索引。
B+Tree 是一种多叉树,只在叶子节点中按主键顺序放真实数据,在非叶子节点中只放索引。每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息。
B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引,它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。
主键索引的 B+Tree 和二级索引的 B+Tree 是有一定区别的
主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。查询时需要先获得主键值,再通过主键索引中的 B+Tree 树查询到对应的叶子节点来获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。
1、B+Tree vs B Tree
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
2、B+Tree vs 二叉树
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。在实际的应用当中,d 值是大于100的,这样就保证了即使数据达到千万级别,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。
而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
3、B+Tree vs Hash
Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。
但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。
索引缺点
需要占用物理空间,索引数量越大,占用的空间就越大;
创建和维护索引要耗费时间,这种时间随着数据量的增加而增大;
索引会降低表增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
什么时候需要索引?
有经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
字段有唯一性限制的,比如商品编码;
经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在 B+Tree 中的记录都是排序好的。
什么时候不需要创建索引?
表数据太少的时候,不需要创建索引;
经常更新的字段不用创建索引,因为如果索引字段频繁修改,那么维护 B+Tree的成本就会上升,会影响数据库性能的。
WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。
字段中存在大量重复数据,比如性别字段,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。这种情况MySQL很可能会通过查询优化器进行全表扫描。
使用前缀索引
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引,比如在一些包含长字符串的字段上用它的前几个字符作为前缀索引,就可以减小索引字段大小,增加一个索引页中存储的索引值,有效提高索引的查询速度。
order by 无法使用前缀索引;
无法把前缀索引用作覆盖索引;
使用覆盖索引
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
所以我们可以针对要查询的字段建立一个联合索引,从而避免回表。
使用自增主键
InnoDB 创建主键索引默认为聚簇索引,数据存放在 B+Tree 的叶子节点上。同一个叶子节点内的各个数据是按主键顺序来存放的,每当有一条新的数据插入时,数据库会根据主键把其插入到对应的叶子节点中。
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至可能发生从一个页面复制数据到另外一个页面的页分裂情况。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。比如一个满的数据页里是1、3、5、9,现在要插入数据7,那就需要把这个页面分割成两个页面,一个是13、一个是579。如果是自增的主键插入就不会出现这种情况。
首先,InnoDB 的数据是按「数据页」为单位来读写的,每个数据页的默认大小是 16KB。
在 文件头里有两个指针,分别指向上一个数据页和下一个数据页,这样相当于把所有的页连接成一个双向的链表。
数据页中的记录会按照「主键」顺序组成单向链表,这样插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。所以,数据页中有一个页目录,起到记录的索引作用,这样就能快速找到所需的记录。
页目录创建过程
首先会把将所有的记录按主键顺序划分成若干个组,每个记录组的最后一条记录就是该组的最大记录,并且会在最后一条记录的头信息中存储这个组记录的总条数。然后用页目录来存储每组最后一条记录的地址偏移量,就相当于一个槽,每个槽指向了不同组的最后一个记录,槽相当于分组记录的索引。因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽里,定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
所以对一个数据页中的记录检索来说,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。
当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。
InnoDB 采用了 B+ 树作为索引底层结构,InnoDB 里 B+ 树中的每个节点都是一个数据页。
只有叶子节点才存放了数据,非叶子节点仅用来存放目录项作为索引。所有节点按照索引键大小排序,构成一个双向链表,便于范围查询;
比如要查找6
从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7)范围之间,所以到页 30 中查找更详细的目录项;
在非叶子节点(页30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页16)查找记录;
接着,在叶子节点(页16)中,通过槽查找记录时,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。
可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。
首先,我讲一下B+树的前世今生。
我们都知道MySQL 的数据是需要持久化到磁盘上的。但是计算机从磁盘中读取数据的速度比较慢,当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到对应的数据然后读入到内存里,那么这个过程会发生多次磁盘 I/O,所消耗的时间也就比较大。所以,我们希望索引的数据结构能在尽可能少的磁盘的 I/O 操作中完成查询工作。所有我们希望这个查询的过程即要少的磁盘IO,又要高效执行范围查找。
所以为了查找的高效率,我们在想索引数据按顺序排列,然后用二分查找法怎么样。因为二分查找每次都把查询的范围减半,这样时间复杂度就从挨个查找的n降到了 O(logn)。
虽然线性排序使用起来比较简单,但是插入新元素的效率太低。因为每插入一个新元素,都要把这个元素之后的所有元素后移一位,如果这个操作发生在磁盘里面那这太夸张了,效率极低。而且每次查找都要不断计算中间的位置。所以我们想有没有一种非线形且适合二分查找的数据结构。
我们自然而然就想到了二叉查找树,二叉查找树的特点是每个节点左子树的所有节点都小于本节点,右子树的所有节点都大于本节点。这样我们在查询数据时,不需要计算中间节点的位置了,只需把要查找的数据和节点的数据进行比较就ok了。那这样不仅查找的效率变高了,在插入新节点时也不会像线性结构那样插入元素之后的元素都向后排列。
但二叉查找树就也不适合作为索引的数据结构,为什么呢,因为二叉查找树存在一个极端情况,如果每次插入的元素都是二叉查找树中最大的元素,那么这些元素就都会往右边排,这样二叉查找树就会退化成了一条链表,它查询的时间复杂度就又退化成了 O(n),而且随着树的高度越来越高,每次查询数据的时候磁盘 IO 次数就会越来越多,导致查询的性能严重下降。
为了解决二叉查找树会在极端情况下退化成链表的问题,后面就有人提出平衡二叉树的概念。
它主要是在二叉查找树的基础上增加了一些条件约束:每个节点左右子树的高度差不能超过 1。如果插入了元素之后导致节点左右子树高度差超过了1,那就自己维持平衡。比如往1节点的右子树是2,现在需要插入3,正常情况插入3的话还是在2子树的右边,那这就不平衡了,所以需要调整,原本1是中间节点,现在把2作为中间节点,1和3分别为2的左右子树,类似于这样的操作来维持平衡二叉树的自平衡状态。这样的话查询操作的时间复杂度就会一直维持在 O(logn) 。
但不管平衡二叉树还是有一定的缺点,因为随着插入的元素的增多,树的高度也会逐渐变高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。根本原因是因为它还是一个二叉树,它的每个节点只能保存 2 个子节点。所以我们在想,能不能树的每个节点可以有多个子节点,那这样树的高度不就下来了吗。
由此就引出了B树的概念,它不再限制每个节点只能有 2 个子节点,而是允许每个节点有 M 个子节点,以此来降低树的高度。这个M 称为 B 树的阶,所以 B 树的话它就是一个多叉树。所以相同数量的节点如果在B树的场景下,树的高度就会低很多,对应的磁盘 I/O 次数也会减少。
但是使用B树作为MySQL索引的结构又有了一些问题, 因为B 树的每个节点都会包含索引+记录,如果用户的记录数据的大小远远超过了索引数据的大小,那我们在查找需要的数据时就不得不从这部分数据上走过,这就需要花费更多的磁盘 I/O 操作次数来读到「有用的索引数据」。而且,当我们查询位于底层的某个节点时,其他不需要的记录数据也会从磁盘加载到内存,这样不仅增多磁盘 I/O 操作次数,也占用内存资源。
所以由此,最终的B+树概念就提出来了,B+树是对B树结构的一些改造,B+树只在叶子节点来存放实际数据索引+记录,而非叶子节点只会存放索引;所有索引也都会在叶子节点里保存一份,叶子节点之间构成一个有序链表;非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)。非叶子节点中有多少个子节点,就有多少个索引;
因为B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,所以在数据量相同的情况下,相比于既存索引又存记录的 B 树结构,B+树的非叶子节点可以存放更多的索引,所以 B+ 树的高度可以比 B 树更低,查询底层节点的磁盘 I/O次数会更少。而且在删除节点的时候,B+树可以直接从叶子节点中删除,这样删除非常快。
而且B+ 树所有叶子节点间还有一个链表进行连接,这种设计在进行范围查找时会更快,比如我们查 1-1 到 1-10 之间的订单,这个时候可以先查找到 1-1 所在的叶子节点,然后利用链表向右遍历,直到找到 1-10的节点,这样就不需要从根节点查询了,进一步节省查询需要的时间。
然后Innodb 里使用的 B+ 树有一些特别的点,比如:
B+ 树的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。
B+ 树点节点内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16 KB。
Innodb 根据索引类型不同,分为聚集和二级索引。他们区别在于,聚集索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚集索引的叶子节点,而二级索引的叶子节点存放的是主键值,而不是实际数据。
因为表的数据都是存放在聚集索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚集索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个,而二级索引可以创建多个。