原文对B+树的解释是很详细的,看的好文章记录转载一下。
原文地址:https://www.toutiao.com/i6947997799594164748/
很多互联网应用都离不开数据库的增删改查(CRUD),实际开发过程中经常因为数据库索引没有建好,导致系统性能问题。了解数据库索引查询数据的底层原理,有利于我们更好地优化系统的查询性能。本文主要以Mysql数据库InnoDB引擎来介绍,关于InnoDB引擎的数据存储格式可以参考前文《Mysql引擎InnoDB数据存储的基本单位是什么? 》。
B+树索引
使用B+树存储数据,磁盘IO更少。B+树中,非叶子节点是不存储数据的,只存储key,这样每个节点能够存储更多的key,使得树的高度更矮,进而读取磁盘次数更少。
假如B+树的一个节点可以存储 1000 个键值,那么3层B+树最多可以存储 1000×1000×1000=10 亿条数据。根节点一般是常驻内存的,如果要在10 亿条数据进行一次单条数据记录的查询,只需要 2 次磁盘 IO。
B+树有利于范围查找和排序。B+树的所有数据都存在叶子节点,而且数据是按顺序存储的,这样使得范围查找、排序查找更加方便。关于B+树的特性,可以参考前文《Java面试常见问题:B-树和B+树 》。
正是因为B+树的这些优点,包括InnoDB在内的很多数据库存储引擎都采用B+树来建立索引。在InnoDB中,数据页之间用双向链表连接,数据记录之间用单向链表连接。由于页之间有双向链表链接,使得扫描数据更加快捷。
B+树的索引
主键构成聚集索引
B+树在叶子节点存储数据,这样的索引是聚集索引。MySQL里默认根据主键创建的索引就是聚集索引,根据主键构建一棵B+树,主键所对应的值直接存在叶子节点中。
由页组成的链表,页之间是双向列表,页里面的数据是单向链表,这种结构组成了主键索引B+树。
对于以下查询语句,上图展示了主键索引的查询过程。
select * from user where id>=18 and id <40
根据主键id的最小值,首先找到起始的页面和数据,之后利用数据记录的单向链表向后搜索,直到主键值达到搜索范围的最大值结束。在搜索范围内的Page页都被载入内存中。
非主键构成非聚集索引
根据主键以外的字段创建的索引一般是非聚集索引,非聚集索引也是用B+树构建的。非聚集索引和聚集索引的唯一不同就是叶子节点中保存的值不是实际的值,而是主键值。
当基于主键以外的字段来查询数据时,引擎先通过 非聚集索引 找到对应的主键值后,再去聚集索引中查找需要的数据。
假设数据库基于非主键字段 num 建立了非聚集索引。对于以下查询语句的搜索过程如上图所示。
select * from user where num=33
首先在非聚集索引的非叶子节点中定位到对应的页,在页中利用单向链表定位到对应的记录。在记录中有对应的主键值,接下来转到聚集索引中查询最终的数据。
总结
最后我们总结一下InnoDB引擎查询指定数据的过程:
- 数据库系统经过解析SQL语句;
- 读取装有非叶子节点的Page页,遍历非叶子节点;
- 随着节点的遍历,会将一个或多个Page页加载到内存,直到定位到这条记录所在的叶子节点;
- 在数据页中遍历单向链表找出该条记录。
在InnoDB 存储引擎,页(Page)是用于磁盘和内存进行数据交换的基本单位,即最小磁盘单位。常见的页类型有数据页、Undo 页、系统页、事务数据页等,本文主要分析的是数据页。
InnoDB的逻辑存储结构
下图为InnoDB引擎的逻辑存储结构图,从上往下依次为:表空间(Tablespace)、段(Segment)、区(Extent)、页(Page) 以及行(Row)。表空间是由各个段组成的,段一般分为数据段、索引段和回滚段等。区是表空间的单元结构,每个区的大小为1MB。
页是组成区的最小单元。页的本质就是一块16KB大小的存储空间,InnoDB把页分为不同的类型,其中用于存放记录的页也称为数据页。
- File Header:文件头部,记录页的通用信息,比如:页的校验和、编号、上一页(FIL_PAGE_PREV )、下一页(FIL_PAGE_NEXT)等;
- Page Header:页面头部,记录页的状态信息,比如:本页有多少条记录、第一条记录的地址、页目录中有多少槽(slot)、最后插入记录的位置等;
- Infimum + supremum:两个虚拟的行记录,Infimum(下确界)记录比该页中任何主键值都要小的值,Supremum (上确界)记录比该页中任何主键值都要大的值;
- User Records:实际存储数据的行记录;
- Free Space:页中尚未使用的空间;
- Page Directory:页面目录,记录各个槽在页面中的地址偏移量;
- File Trailer:文件尾部,校验页的完整性。
将页连成双向链表
一张表中有成千上万条记录,一个页只有16KB,所以需要好多页来存放数据。不同页之间构成了一条双向链表,File Header 的 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 分别代表本页的上一个和下一个页的页号,即链表的上一个以及下一个节点指针。
最大记录和最小记录
Infimum 和 supremum 是两个伪行记录,不记录实际数据,因此也不放在User Records区域中。这两条记录的作用就是确定了当前数据页记录主键的最小值和最大值,这两个值在页创建时被建立,并且在任何情况下不会被删除。
页中插入记录
在一开始生成页的时候,页中并没有User Records这个部分。每当插入一条记录,都会从Free Space部分中申请一个记录大小的空间划分到User Records部分。
当Free Space部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。总之,User Records 和 Free Space 之间是此消彼长的关系。
除了Java语法和并发编程方面这些必考内容,Java面试还经常会问到关于数据结构方面的问题。本文就来介绍两个在数据库和文件系统中常用的数据结构:B-树和B+树。需要注意的是:“B-树”中的短横不是减号,B表示平衡(Balance)的意思。
B-树
B-树是为了磁盘或其它存储设备而设计的一种平衡多叉搜索树。B-树与平衡二叉搜索树最大的不同在于,B-树的结点可以有许多孩子。B-树可以在O(logn)的时间复杂度内,实现插入、删除等动态操作,相比平衡二叉树,大大减少了IO的次数。
M阶B-树(M>2),代表一个树结点最多有M个子结点(查找路径),上图为3阶B-树。
B-树需要满足以下规则:
- 排序方式:每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它;
- 子结点数:根结点的子结点数为[2, M],其他非叶结点的子结点数为[M/2, M],向上取整;
- 关键字数:非叶结点的关键字数量为子结点数-1,如上图每个非叶结点有2个关键字,分成3个区间,分别指向3个子树;
- 叶子结点:所有叶子结点均在同一层,或者说根节点到每个叶子节点的长度都相同;
另外B-树的每个结点,除了有关键字key以外,还有value,也就是关键字记录的指针(地址)。
苹果电脑的HFS+文件系统,就采用了B树作为文件系统的索引。
B+树
B+树是B-树的一种变形,其与B-树的区别主要在于:
- 非叶子结点的子树指针与关键字个数相同;
- 非叶子结点的子树指针 P[i],指向关键字值属于 [K[i], K[i+1]) 的子树,而B-树的子树结点关键字不包括 K[i];
- 所有叶子结点增加了一个链指针;
- 所有关键字都在叶子结点出现。
上图是一个 M=3 的 B+树,非叶子结点的关键字个数和子树指针都是3个。第一个非叶子结点(5,10,20)的第一个指针 P1 指向的子树,只有一个叶子结点,P1对应的关键字5又出现在了叶子结点的关键字中。
B+树的应用
B+树适合应用于操作系统的文件索引和数据库索引。B+树相比于B树,在文件系统和数据库系统当中更有优势,原因如下:
- IO读写代价低
B+树的非叶结点不保存关键字记录的指针,只进行数据索引,B+树每个非叶节点所能保存的关键字大大增加。一次性读入内存中的关键字越多,也就意味着I/O读写次数越少。 - 查询效率稳定
B+树只有叶子结点保存了关键字记录的指针,任何关键字记录的查找都必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,数据查询效率相比B-树更加稳定。 - 有利于数据库扫描
B+树只需要遍历叶子结点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的区间查询,B+树有着更高的性能。
Mysql的InnoDB和MylSAM存储引擎都是用B+树实现索引结构。