SQLite的核心思想是使用B-tree实现表和索引的存储。为什么B-tree这种数据结构更适合于数据库呢?
- 搜索数据非常快(时间消耗随着数据规模的增大呈对数级增长)
- 对于已经找到的值,插入/删除操作非常快(每次插入/删除后再平衡时间恒定)
- 能快速遍历值(与哈希表不同)
B-tree
B-tree的结构与传统的二叉树不同(“B”可能代表其发明人的名字,也可能表示“平衡”的意思)。下面是B-tree的示意图:
与二叉树不同,B-tree的每个节点不只有两个子节点。每个节点最多可以有m个子节点,其中m称为树的“阶数”。为了让树保持最大程度的平衡,我们也会规定节点必须有至少m/2个子节点(向上取整)。下面是一些特殊情况:
- 叶子节点没有子节点
- 根节点的子节点数可以比m少,但是必须至少有两个子节点
- 如果根节点是一个叶子节点(是整棵树中唯一的节点),那么它也不能有子节点
上图的B-tree是SQLite用来存储索引的。如果想存储表,SQLite使用的是B-tree的变种,叫B+ tree。
B-tree | B+ tree | |
---|---|---|
Pronounced | “Bee Tree” | “Bee Plus Tree” |
Used to store | Indexes | Tables |
Internal nodes store keys | Yes | Yes |
Internal nodes store values | Yes | No |
Number of children per node | Less | More |
Internal nodes vs. leaf nodes | Same structure | Different structure |
在实现索引之前,我们只讨论B+ tree,但是我们也把它叫做B-tree。
有子节点的节点叫做“中间”节点。中间节点和叶子节点的结构不同:
For an order-m tree… | Internal Node | Leaf Node |
---|---|---|
Stores | keys and pointers to children | keys and values |
Number of keys | up to m-1 | as many as will fit |
Number of pointers | number of keys + 1 | none |
Number of values | none | number of keys |
Key purpose | used for routing | paired with value |
Stores values | No | Yes |
B-tree元素插入示例
让我们通过一个示例来看看B-tree在插入元素时是如何变化的。为了让事情变得简单,我们把B-tree的阶数定为3,这意味着:
- 每个中间节点最多有3个子节点
- 每个中间节点最多有2个键
- 每个中间节点最少有2个子节点
- 每个中间节点最少有1个键
一个空的B-tree只有一个节点,即根节点。根节点开始时作为叶子节点没有键值对:
每当我们插入一组键值对,它们都会按大小顺序存放在叶子节点中。下图是插入两组键值对后B-tree的情况:
假设叶子节点的容量是两组键值对。此时,我们再插入第三组键值对,叶子节点会拆分成两个节点,同时每个节点只拥有原先一半的键值对。随后,生成一个新的中间节点,之前两个节点将成为其子节点。该中间节点现在就成为了根节点。
此时,中间节点有1个键和两个指向子节点的指针。如果想查找小于等于5的键,我们可以从左子节点中找。如果想查找大于5的键,我们可以从右子节点中找。
现在,让我们插入“2”这个键。我们先查它应该存放在哪个叶子节点中,此时我们会找到左子节点。但是当前该节点已经满了,因此我们拆分左子节点,并在其父节点中创建一个新的键。
让我们继续添加键“18”和“21”。此时,我们又到了必须再次分割的子节点的时刻,但是父节点中已经没有空间再容纳新的键和指针了。
解决办法是将根节点拆分为两个中间节点,然后创建新的根节点作为它们的父节点。
总结
从上面的示例中我们可以得出:
- 只有在拆分根节点时树的深度才会增加
- 每个叶节点具有相同的深度和近似数量的键值对,因此树能够保持平衡,从而实现快速搜索
在我们实现B-tree的插入功能之前,暂时先不讨论如何从树中删除键。
当我们实现B-tree时,每个节点对应一页,根节点存放在第0页。指向子节点指针可以简单地用包含该子节点的页码来代替。
下一节我们将开始实现B-tree!
原文链接:Let’s Build a Simple Database: Part 7 - Introduction to the B-Tree