B-树与B+树
B树的引入
从磁盘查找数据效率低,一般是什么原因?
影响IO效率因素:读写数据越大速度越慢;读写次数越多速度越慢。
索引 ⇒\Rightarrow⇒ 更快的查询数据
如何来设计一个文件系统 k:v ⇒\Rightarrow⇒ 用什么数据结构来实现?
线性表:
- 顺序表
- 优点:支持随机访问(O(1) 时间访问任意元素)。
- 缺点:插入/删除效率低(平均需移动 O(n) 个元素)。
- 链表
- 优点:插入/删除效率高(O(1) 若已知位置)。
- 缺点:不支持随机访问,必须从头遍历。
哈希表:
-
优点:等值查询比较快
-
缺点:
-
哈希冲突后,数据散列不均匀,产生大量线性查找,性能下降;
-
不支持范围查询。
-- 范围查询是指:你不需要知道某个字段的确切值,而是想查找在一个数值区间内的所有记录。 -- 查询年龄在 20 到 30 之间的所有人 SELECT * FROM users WHERE age BETWEEN 20 AND 30;
根本原因:哈希表是无序的
哈希表的工作原理是:哈希表通过一个哈希函数将键(key)映射为一个位置(哈希值),然后存储数据。
例如:
hash(25) = 9287 → 存储在第 9287 号桶 hash(26) = 103 → 存储在第 103 号桶 hash(27) = 5000 → 存储在第 5000 号桶
虽然 25, 26, 27 是连续的数值,但它们的哈希值完全随机分布,没有任何顺序关系。
范围查询需要“顺序性”,而哈希表没有
要高效执行 WHERE age > 25,数据库需要:
- 找到第一个 age > 25 的记录;
- 然后顺序读取后续所有符合条件的记录。
但哈希表中:
- 数据不是按 age 排序的;
- 无法从“25 的下一个”开始扫描;
- 每个键值的位置是独立计算的。
所以你无法利用哈希索引进行“区间扫描”或“顺序遍历”。
而必须:
- 遍历每一条记录;
- 计算其 age的哈希值;
- 查看是否存在于哈希表中;
- 再判断 age > 25。
这实际上等同于全表扫描,索引完全失效!
-
树:
-
BST
- 退化成链表:如果按顺序插入数据(如 1, 2, 3, 4, 5),BST 会退化成一个链表,查找时间复杂度从理想的 O(log n) 变成 O(n),完全丧失了索引的优势。
- 查询效率极低:此时查找某个值需要遍历所有节点,和全表扫描无异。
例如:插入有序数据后,BST 会变成一条斜线,查找第6个节点需要6次比较,性能与无索引相当
-
AVL
- 严格平衡:AVL 树要求左右子树高度差不超过1,通过频繁的旋转操作(左旋、右旋)维持平衡。
- 插入/删除成本高:每次插入或删除都可能触发 O(log n) 次旋转,尤其在写密集型场景下,性能开销极大。
- 适合读多写少,不适合数据库:数据库需要频繁更新索引,AVL 的高维护成本使其不适用。
虽然查询快(O(log n)),但写操作的旋转代价太高,拖累整体性能。
-
红黑树
- 允许一定失衡:红黑树通过颜色标记和规则放宽平衡条件,树高最多为 2log(n),比 AVL 更“宽松”,减少了旋转次数。
- 树仍然太“瘦高”:对于海量数据(如10亿条),红黑树的深度仍可达 60层以上。
- 磁盘IO次数太多:数据库索引通常存储在磁盘上,每次节点访问可能是一次磁盘IO。60次磁盘IO在数据库场景下是不可接受的。
红黑树虽然写性能优于AVL,但树太高 → IO次数多 → 查询慢。
什么是B树
B-Tree
B树属于多叉树又名平衡多路查找树(查找路径不止两个),数据库索引技术里大量使用着B树和B+树的数据结构。
满足下列要求的m叉树:
-
树中每个结点至多有 m 个孩子结点(即至多有 m-1 个关键字)
-
每个结点的结构为:
其中“4阶”指的是该树中一个节点最多可以拥有4个子节点,更详细地说,“阶”(通常用 m 表示)定义了B树的分支能力。
-
除根结点外,其他结点至少有 m/2 个孩子结点
-
若根结点不是叶子结点,则根结点至少有两个孩子结点
-
所有叶子结点都在同一层上,即B树是所有结点的平衡因子均等于0的多路查找树。
B树的查找
磁盘预读
- 内存跟磁盘发生数据交互的时候,一般情况下有一个最小的逻辑单元,称之为页,datapage;
- 页一般由操作系统决定是多大,一般是4K或者8K,我们在数据交互时,可以取页的整数倍来进行读取。
- 第一次IO,首先加载磁盘块1, 28>16 ,28<34 ,找到磁盘块1 →\rightarrow→ p2
- 第二次IO,加载磁盘块3,28>25 ,28<31 ,找到磁盘块2 →\rightarrow→ p2
- 第三次IO,加载磁盘块8, 28=28 ,返回
B+树的引入
B树的缺点
-
非叶子节点也存数据 → 树更高 → 查询慢
假设每个磁盘页是 16KB,每个索引项(key + data)占 200 字节。
- 每个节点最多能存:16KB / 200B ≈ 80 个 key+data
- 如果我们改用 B+树,非叶子节点只存 key 和子节点指针(比如 key+指针 = 20B),则能存 16KB / 20B ≈ 800 个 key
结果:I/O 更多,性能更差。
- B树:分支少 → 树更高(比如 4 层)
- B+树:分支多 → 树更矮(比如 3 层)
-
不支持高效范围查询
SELECT * FROM user WHERE id BETWEEN 1000 AND 9999;
-
在 B树中怎么做?
找到 id=1000 → 从根开始查找
找到 id=1001 → 再从根开始查找
……
找到 id=9999 → 每次都要从根遍历到叶子
因为 叶子节点之间没有连接,无法“顺着找下一个”。
-
B+树怎么解决?
- 非叶子节点只存 key 和指针 → 更多分支 → 树更矮 → I/O 更少
- 所有数据都在叶子节点,且 叶子节点用双向链表连接
- 范围查询时:
- 先查 id=1000(3次 I/O 到叶子)
- 然后 顺着链表往后扫,直到
id=9999
- 后续不再需要从根查找!
一次定位 + 顺序扫描 = 极快的范围查询
-
-
查询效率不稳定
-
在 B树 中:
- 如果你要查的 id=500,它可能正好在根节点 → 1 次 I/O 就返回
- 但 id=900 在叶子 → 要走到底(4 次 I/O)
不同查询耗时差异大,不稳定
-
而在 B+树 中:
- 所有查询都必须走到叶子节点
- 路径长度一致 → 性能稳定可预测
-
B+树 ⇓\Downarrow⇓
什么是B+树
B+树保留了B树的基本特性,但对节点数据的存储方式进行了调整,以提升磁盘I/O效率和范围查询性能。它广泛应用于数据库(如MySQL的InnoDB引擎)和文件系统中。
B+树的主要特点:
-
非叶子节点仅存储索引:B+树的非叶子节点不存储实际的数据,只存储关键字和子节点指针,起到索引的作用。这使得每个节点可以容纳更多的关键字,从而降低树的高度,减少磁盘I/O次数。
-
所有数据存储在叶子节点:所有实际的数据记录都存储在叶子节点中,且叶子节点包含了全部关键字的信息以及指向对应数据记录的指针。
-
叶子节点构成有序链表:B+树的叶子节点之间通过指针连接成一个有序的双向链表。这一特性使得范围查询(如BETWEEN、ORDER BY)变得非常高效,只需遍历链表即可。
-
所有叶子节点位于同一层:B+树是高度平衡的,所有叶子节点都处于相同的深度,保证了查询效率的稳定性,任何查找、插入、删除操作的时间复杂度均为O(log n)。
-
更适合磁盘存储:由于节点大小通常与磁盘块大小对齐,且树的高度较低,B+树能显著减少访问磁盘的次数,非常适合处理大规模数据。
-
查询效率稳定:查找操作必须最终到达叶子节点才能命中,因此每一次查找的路径长度基本一致,性能稳定,等价于在关键字全集上进行一次二分查找。