MySQL查询更新和存储
当我们执行一条sql时会经过如下流程,可以看到数据最终是以文件的形式存在
查询缓存:之前查询过的语句会以KV的形式缓存在内存中。不推荐使用缓存
分析器:知道你要做什么?先做词法分析,识别sql语句中的关键字,再做句法分析,判断sql语句是否符合句法
优化器:主要作用是决定如何使用索引
执行器:进行权限校验,调用存储引擎。执行器会以行为粒度,调用存储引擎,执行sql
存储引擎:将执行器的指令落实在数据文件上
存储引擎 | 特点 |
---|---|
MyISAM | MySQL5.5.5之前的默认存储引擎,插入数据快,空间利用率高 |
InnoDB | MySQL5.5.5之后的默认存储引擎,支持事务,外键,支持崩溃修复和并发控制 |
Memory | 所有数据都在内存中,速度快 |
Archive | 数据压缩,空间利用率高,插入速度快,不支持索引,查询性能差 |
一条更新语句的大概流程如下图所示,后面的文章我们会详细分析每个部分,先有个整体的映像极客
当我们在数据库中建立一张student表时,在文件系统中会创建出如下几个文件
这些数据最终会持久化到文件中,那么这些数据在文件中是如何组织的?难道是一行一行追加到文件中的?其实并不是,数据其实是存到页中的,一页的大小为16k,一个表由很多页组成,这些页组成了B+树
一个表的存储方式如下所示
一行一行的数据组成页,页再组成区,区再组成段,便于管理
页:页是innodb磁盘管理的基本单位,innodb每个页的大小为16k
区:由64个连续的页组成,每个页的大小为16kb,即每个区大约为1MB
段:常见的段有数据段(B+树页节点),索引段(B+树非页节点),回滚段等
我们先从微观的角度看一下页是如何存储数据的?
一个页的格式如下所示
名称 | 说明 |
---|---|
文件头 | 表示页的信息 |
页头 | 表示页的状态信息 |
最小和最大记录 | 两个虚拟的记录,表示页中的最小记录和最大记录 |
用户记录 | 存储行记录内容 |
空闲空间 | 页中还没有被使用的空间 |
页目录 | 对用户记录进行索引 |
文件尾 | 校验页是否完整 |
数据会源源不断的插入User Records,当插满时,Free Space也就不复存在了。
页面里面的记录是按照主键值从小到大排序的,构成一个单链表
有的小伙伴可能就会问了,我建的表没有主键,那表的数据该如何组织呢?
- 首先判断表中是否有非空唯一索引,如果有则该列即为主键
- 如果没有,则添加一个名为row_id的隐藏列作为主键
页面之间通过双链表连接到一起
当我们在一个页中查找数据的时候,难道需要一个一个的遍历链表?
当然不需要,为了提高查找的速率,mysql会把数据进行分组,并用页目录记录每个分组最大记录的地址
每个分组有多少条数据呢?
- 第一个分组中的记录只能有1条记录
- 最后一个分组中记录条数范围只能在1-8之间
- 剩下的分组中记录条数范围只能在1-8之间
如下图所示
蓝色的部分为主键及其对应的数据
当查找记录的时候,先通过页目录找到对应的分组,然后再遍历分组内的链表
举个例子我想查找主键为10的用户记录,5个槽的编号为0,1,2,3,4,查找的过程如下
- 先二分得出槽中间的位是(0 + 4) / 2 = 2,2号槽里面的最大记录为8,我们需要从2号槽后面继续搜索记录
- 3号槽和4号槽中间的位是(3 + 4) / 2 = 3,3号槽里面的最大记录为12,12 > 10,所以要查找的记录在3号槽
- 但是用户记录间的链表是单向的,所以我们可以先到槽2中的8记录,然后沿着链表开始遍历,直到找到目标记录
虽然在一个页中查找数据由于数据目录的存在很快了,但是在一个表中查找数据会不会还是很慢呢?毕竟要沿着链表遍历所有的数据页
MySQL当然不允许这种事情发生,既然我们能给记录建立目录加快查找速度,我们就能给页建立目录加快查找速度,目录的格式为每个页中的最小主键id及其对应的页号(就是页面的地址)
目录也是存在数据页中的,大小为16kb。所以有可能有多个目录,当目录过多时,我们还可以给目录建立目录。如下图所示
这不就是一颗树吗?叶子结点存储记录,非叶子结点存储主键及对应的页地址。这颗树其实就是一颗B+树
以上图为例,我们在表中查询主键为5的数据,查询过程如下
先到根目录中去查,接着到页30去查,再定位到页16,最终查找到记录
接着我们再从宏观角度分析一下数据的存储,这样便于我们分析问题
当使用MyISAM存储数据时,数据和索引是分开的,B+树上储存的是对应记录的地址
当使用InnoDB存储数据时,数据和索引是在一起的,即聚集索引。当然你可以对某个字段建立非聚集索引
聚集索引的叶子结点存储的是用户数据,而非聚集索引叶子结点存储的是被索引的列值及其对应的主键值
当使用主键查询记录时,只需要遍历聚集索引即可。而当使用非聚集索引查询数据时,先遍历非聚集索引找到记录的主键值,再根据主键值到聚集索引上遍历获取数据,即回表
最后为了加深大家的理解,放一个我在面试中遇到的一个挺有意思的问题
假设一颗B+树的主键为bigint,一行数据的大小为1kb,树高为3层,那么它能存储多少数据?
mysql默认一个节点的长度为16KB,一个整数字段索引的长度为8B,另外每个索引还跟着6B的指向其子树的指针。树高为3层,那么前2层用来存索引项,最后1层用来存数据,能存储的数据为
( 16 K B / ( 8 B + 6 B ) ) 2 ∗ ( 16 K B / 1 K B ) = 117 0 2 ∗ 16 = 21902400 (16KB / (8B + 6B)) ^2 * (16KB / 1 KB) = 1170^2 * 16 = 21902400 (16KB/(8B+6B))2∗(16KB/1KB)=11702∗16=21902400
1GB=1024MB
1MB=1024KB
1KB=1024B(字节)
1B=8bit(位)
参考博客
[1]http://liuqh.icu/2019/04/03/db/innodb-storage/
[2]https://bbs.huaweicloud.com/blogs/317532