MySQL数据库
本文最后更新于 2025-02-23,文章内容可能已经过时。
1、B树和B+树的区别
B Tree
B+Tree
B+ Tree Visualization B-Tree Visualization
节点之间排序
B+树叶子节点之间会有指针,B树没有
B+树非叶子节点的元素都冗余了一份在叶子节点上
Innodb中的B+树如何生成的呢
以Mysql版本5.7.16展示
InnoDB引擎下,一页默认大小为16384/1024 = 16KB
执行命令 SQL语句 show global status like 'Innodb_page_size';
每次从Innodb中取数据的时候,最小单位是 一页 每次读取16KB
写数据时,最少要在内存开辟16KB空间
Mysql创建表时,底层仍然会认为是BTree,会认为B+Tree是BTree的一个升级版
插入的时候,我的insert into语句是乱排,仍可以根据主键索引 排列
insert into t1 values(4,3,1,1,'d');
insert into t1 values(1,1,1,1,'a');
insert into t1 values(8,8,8,8,'h');
insert into t1 values(2,2,2,2,'b');
insert into t1 values(5,2,3,5,'e');
insert into t1 values(3,3,2,2,'c');
insert into t1 values(7,4,5,5,'g');
insert into t1 values(6,6,4,4,'f');
插入完成之后,会自动根据 主键索引排序
而Mysql从这张表往内存中读数据,也不是一行一行读取,而是一页一页读取
那这一行数据有多大呢?
CREATE TABLE t1(
`a` int primary key,
`b` int,
`c` int,
`d` int,
`e` VARCHAR(20)
) ENGINE=InnoDB;
创建表时,a,b,c,d,e,五个字段,其中四个int类型,一个int类型占4个字节,一个varchar类型,就默认字符集是utf8mb4,一个varchar类型占4个字节,那么一行数据的大小就是 (4*4)+4 = 20b. 1KB = 1024b
一页 = 16KB
取数据时,一页一页取,那么插入的这8条数据都包含在这一页当中.只需要取一次,进行一次磁盘IO就可以放入内存中
假设,这一页中,架构如下
a为1,2 为一组 4,5是一组,数据与数据之间指针相连,那么页目录的值就是这一组第一条数据的主键索引的值
假设,此时执行一条sql语句 select * from t1 where a = 8,会把这一页的数据从磁盘中放入内存,首先判断这个值为8的a会在哪一组
会直接从页目录为4遍历查找,因为这个组下就2条数据,只会遍历两次,如果没有找到,那就说明这张表中没有a=8,不会在去页目录为1的数据组下循环查询。 这里运用的就是 空间换时间的设计
假设我一页只能存这4条数据,再往里面插入第五条数据时,会新开一页 ,执行 insert into values(3,3,2,2,'c');
会把插入的数据放入新开辟的一页吗? 会先判断,主键a = 3 , 由于插入时会自动排序,则会把这条数据插入页目录4的组下,会把a=5这条数据放入新的一页,期间断开指针重新插入指针,会有一定的性能影响,所以推荐 主键策略会自增
现在有两页 执行SQL select * from t1 where a = 6
innodb执行时,会把第一页加载查询,发现第一页没有会根据指针去第二页查询
假设此时 海量数据新增 不停地新建页 链表也会不断地增大,查询时也需要不断地从磁盘中一页一页的读取数据。效率低下
优化思路
现在有两页,新建一个页目录 存放 每一页中 页目录最小的值,上图中 最小值是 1、3
那么新增的页目录就 记载1、3,并指向对应的页(不想再画图了)
此时在执行select * from t1 where a = 6 首先会判断存放页目录值的这一页,通过判断结果去对应的页目录查找
那这一页又能存放多少页目录的值呢 ,假设主键a类型是int,占4字节,指针占6字节, 而一页大小又是16KB
16*1024/10=1638,也就是说,存放主键+指针这样一对一对的数据,可以存放1638组
高度为3的B+Tree能存多少数据呢?
上图显示,叶子节点主要存放的就是数据,根节点存放的则是主键值
假设在一张表中,每一条记录大小1KB,主键是int类型,那么就是1638*(16/1) = 26208,一个2层B+Tree可以存放的数据26208条数据
那三层B+Tree呢
在顶层节点中,假设有两个组,其中一组记录最小的页的主键值,另一组记录另一页的主键值,那么就是 1638*1638*16
达到这么大的值,就不建议继续通过索引来优化,需要进行分库分表了,因此尽量把B+Tree维护在两层
Innodb是如何支持范围查找能走索引的?
这种把数据存放在叶子节点,非叶子节点存放数据索引的三层B+Tree,称之为主键索引或者 聚集索引/聚簇索引
此时执行一条SQL select * from t1 where a = 6 ,就会根据顶层的索引页往下查找
这是根据主键a建立的主键索引 ,执行select * from t1 where b = 6,则走不了这个索引,只能一条条遍历
那执行select * from t1 where a > 6 或者 select * from t1 where a < 6呢?
可以看到,两条SQL语句走的都是主键索引.
为什么要遵循左前缀法则才能利用到索引?
先执行SQL select * from t1;
表中有8条数据并按照主键排序
执行SQL create index idx_t1_bcd on t1(b,c,d); 为BCD字段创建一个联合索引
在执行SQL explain select * from t1 WHERE b= 1 and c = 1 and d = 1;
发现是可以走这个创建的联合索引。
它的底层会生成一个B+Tree,并按照升序排列,可以看到表中B字段的值有重复出现,这时会比较C字段排序,以此类推,生成的B+Tree如下图
执行SQL select * from t1 where b = 1 and c = 1 and d = 1 传入的条件就是 111 这样,取出对应的索引页依次查找,但是因为是select * 即使找到了对应的页中的数据,也只有bcd三个字段。所以需要再下方挂主键a索引的值
之后根据a的值去主键索引找,拼接后返回。这个过程就称之为 回表查询 这就是innodb里的回表
那为什么要遵循最左前缀法则呢?
执行 select * from t1 where b = 1 and c = 1 and d = 1 传入的bcd是111可以走聚合索引.
那select * from t1 where d = 1 and c = 1 and b = 1 把这几个字段调换位置
照样可以走聚合索引
依次执行
explain select * from t1 WHERE b= 1; #传入 1** 可以走索引
explain select * from t1 WHERE d= 1; #传入 **1 不行
explain select * from t1 WHERE c= 1; #传入 *1* 不行
explain select * from t1 WHERE b= 1 and d = 1; #传入 1*1 可以
explain select * from t1 WHERE b= 1 and c = 1; #传入 11* 可以
explain select * from t1 WHERE c= 1 and d = 1; #传入 *1* 不行
如上图B+Tree可以发现,从索引最左开始匹配, 如果不匹配,就停止后面的比对。
运行结果如下