跳到主要内容

索引技术演进

信息
2024年8月14日 · ·

索引主流技术

B 树(B-Tree)

B 树是一种自平衡的多叉树数据结构,所有叶子节点在同一层级,设计目的是为了高效存储和检索数据。

  • 特点
    • 每个节点包含多个键和子节点指针。
    • 维护排序的键列表,并通过二分查找进行快速查询。
    • 支持动态插入、删除和查找操作。

B+ 树(B+ Tree)

B+ 树是对 B 树的一种改进,区别在于:

  • 所有值都存储在叶子节点:非叶子节点仅存储键和指向子节点的指针。
  • 叶子节点通过链表连接:方便范围查询。
  • 优势:范围查询更高效;叶子节点有序链表链,易于顺序遍历。

B*树(B* Tree)

B* 树是对 B+ 树的进一步改进,目的是提升节点分裂和空间利用率。

  • 主要改进
    • 节点分裂改为三分之一:节点分裂只有在兄弟节点也满的情况下才发生。
    • 增加了兄弟节点的分裂:分裂时节点向右兄弟节点借键,增加了空间利用率。
  • 优势
    • 更高的节点空间利用率。
    • 插入和删除操作造成的调整次数减少。

LSM 树(Log-Structured Merge-Tree)

LSM 树是一种用于高写入负载优化的数据结构,广泛应用于现代数据库(如 Cassandra、LevelDB)。

  • 特点
    • 写操作首先写入内存中的 MemTable,并定期刷新到磁盘生成 SSTable。
    • 通过异步合并和压缩策略提升写性能。
  • 优势
    • 高效地处理大规模写入操作。
    • 读取操作通过内存和磁盘多级机制进行,综合性能优越。

B 树

B 树是一种自平衡的树数据结构,主要用于数据库和文件系统中,以保持数据的有序性并允许高效的搜索、顺序访问、插入和删除操作。

关键特点

  • 平衡性:所有叶子节点都处于同一层,保证了树的高度平衡。
  • 节点度:每个节点可以有多个子节点。内部节点包含指向子节点的指针,叶子节点则包含实际数据。
  • 键的数目:每个节点包含的键数目有明确的最小和最大范围。假设 B 树的阶数(degree)为 tt,则每个节点至少有 t1t-1 个键,至多有 2t12t-1 个键。
  • 时间复杂度:搜索、插入和删除操作的时间复杂度均为 O(logn)O(\log n),其中 nn 是树中元素数目。

这些特点使得 B 树非常适合用于磁盘存取和其他基于块存储的应用。

B 树结构

  • 节点组成
    • 每个节点包含若干个键值(keys)和指向子节点的指针(pointers)。
    • 每个节点最多有 2t12t-1 个键和 2t2t 个子节点。这里 tt 被称为 B 树的最低度数(minimum degree)。
  • 根节点
    • 特殊的,根节点可以有少于 t1t-1 个键,但其他节点必须至少有 t1t-1 个键。
  • 叶子节点
    • 叶子节点不包含子节点指针,只包含实际存储的数据。
  • 分裂与合并
    • 当插入键导致一个节点的键数超过最大值 2t12t-1 时,需要进行节点分裂(split)。分裂会将中间键上移至父节点,并将分裂的节点分成两个新的节点。
    • 当删除键导致一个节点的键数少于最小值 t1t-1 时,可能需要合并(merge)节点或从兄弟节点借键,以维持树的平衡。
  • 顺序保持
    • 在每个节点内部,所有键按升序排列。指向子节点的指针将节点分成 k+1k+1 个区间,其中每个区间都包含比前一个键大但比后一个键小的所有键。

例如,假设 t=3t = 3(即每个节点可以包含 2 到 5 个键),那么每个节点可能的结构如下:

  • 一个节点可能包含 3 个键:[k1, k2, k3],并有 4 个指向子节点的指针:[P0, P1, P2, P3]
  • 这些指针分隔键的范围,使得所有属于 P0 的值小于 k1,属于 P1 的值在 k1k2 之间,依此类推。

以上结构特性使得 B 树在插入、删除和查找操作时能够高效地保持平衡和有序性。

B 树操作

B 树的主要操作包括插入、删除和查找。每种操作都设计成在保持树的平衡和结构完整性的同时进行。

查找(Search)

查找操作在 B 树中类似于在二分搜索树中的查找:

  • 从根节点开始,逐个比较节点内的键值。
  • 如果找到目标键,返回对应的节点。
  • 如果目标键小于当前节点内的某个键,则转向左子节点进行递归查找;若大于,则转向右子节点。
  • 如果到达叶子节点仍未找到目标键,则说明目标键不存在。

插入(Insert)

插入操作需要确保插入后的树仍然是平衡的:

  • 首先,在树中找到插入键应当位于的叶子节点位置。
  • 将新键插入该叶子节点中,使节点内的键保持有序。
  • 如果插入后该节点的键数超过最大限制(2t12t-1 个键),则进行节点分裂(split)。分裂将节点中间的键上移至父节点,原节点分裂为包含前 t1t-1 个键和后 t1t-1 个键的两个新节点。
  • 如果分裂引起父节点也超过键数限制,则对父节点继续进行分裂,必要时一直递归到根节点。

删除(Delete)

删除操作稍微复杂些,因为需要保持树的平衡:

  • 直接删除
    • 如果目标键在叶子节点中,直接删除该键。
    • 如果目标键在内部节点中,情况较为复杂:
      • 如果待删除键的直接前驱或后继在对应的子树中存在并且包含至少 tt 个键,则用前驱或后继键替换该键,并递归删除前驱或后继节点中的键。
      • 否则,将目标键与其子节点合并,并递归删除目标键。
  • 借键
    • 如果删除操作导致某个节点的键数小于最小限制(t1t-1 个键),需要从兄弟节点借键以保持平衡。兄弟节点必须有至少 tt 个键。
    • 借键后,需要调整父节点和相关子节点的键值和指针。
  • 合并
    • 如果借键不可行,则将受影响的节点与相邻的兄弟节点合并,同时将父节点中的一个键下移至合并后的节点。

复杂度分析

无论是查找、插入还是删除操作,B 树的高度始终保持在 O(logn)O(\log n) 的范围内(nn 为树中的键数量)。因此,这些操作的时间复杂度均为 O(logn)O(\log n),保证了高效的性能,尤其适用于需要频繁读写的大规模数据存储环境,如数据库和文件系统。

通过上述操作,B 树能够高效地维护其平衡和有序性,并提供快速的查找、插入和删除操作。

B 树简化实现

class BTreeNode:
def __init__(self, t, leaf=False):
self.t = t
self.leaf = leaf
self.keys = []
self.children = []

def __repr__(self):
return f'Node(keys={self.keys}, leaf={self.leaf})'

class BTree:
def __init__(self, t):
self.root = BTreeNode(t, leaf=True)
self.t = t

def search(self, k, x=None):
if x is None:
x = self.root
i = 0
while i < len(x.keys) and k > x.keys[i]:
i += 1
if i < len(x.keys) and k == x.keys[i]:
return True
elif x.leaf:
return False
else:
return self.search(k, x.children[i])

def insert(self, k):
root = self.root
if len(root.keys) == (2 * self.t) - 1:
new_root = BTreeNode(self.t)
new_root.children.append(self.root)
self.split_child(new_root, 0)
self.root = new_root
self._insert_non_full(self.root, k)

def split_child(self, x, i):
y = x.children[i]
z = BTreeNode(y.t, y.leaf)
x.children.insert(i + 1, z)
x.keys.insert(i, y.keys[self.t - 1])
z.keys = y.keys[self.t : (2 * self.t) - 1]
y.keys = y.keys[0 : self.t - 1]
if not y.leaf:
z.children = y.children[self.t : (2 * self.t)]
y.children = y.children[0 : self.t]

def _insert_non_full(self, x, k):
i = len(x.keys) - 1
if x.leaf:
x.keys.append(None)
while i >= 0 and k < x.keys[i]:
x.keys[i + 1] = x.keys[i]
i -= 1
x.keys[i + 1] = k
else:
while i >= 0 and k < x.keys[i]:
i -= 1
i += 1
if len(x.children[i].keys) == (2 * self.t) - 1:
self.split_child(x, i)
if k > x.keys[i]:
i += 1
self._insert_non_full(x.children[i], k)

# 示例
btree = BTree(3)
keys = [10, 20, 5, 6, 12, 30, 7, 17]

for key in keys:
btree.insert(key)

print("搜索 12:", btree.search(12)) # 输出:搜索 12: True
print("搜索 100:", btree.search(100)) # 输出:搜索 100: False

B+ 树

B+ 树是 B 树的另一种变种,主要用于数据库和文件系统中。它在 B 树的基础上进行了优化,能够更高效地处理范围查询和顺序访问。

B+ 树结构

  • 节点类型
    • B+ 树有内节点(内部节点)和叶子节点两种类型。
    • 内节点只包含键值和子节点指针,不直接存储数据。
    • 叶子节点包含所有实际数据,并且叶子节点之间通过链表相互连接,便于顺序访问。
  • 键值分布
    • 内节点用于引导搜索路径,键值将搜索空间分为不同的子区间。
    • 叶子节点存储所有的键值对(或指向数据记录的指针),这些键值按照顺序排列。
  • 节点度数
    • 每个节点(内节点和叶子节点)的度数有一个固定范围,取决于树的阶数 t 。
    • 内节点至少包含 t-1 个键,至多包含 2t-1 个键。
    • 叶子节点至少包含 t-1 个键,至多包含 2t-1 个键。

B+ 树操作

查找(Search)

B+ 树的查找操作分为两部分:在内节点中导航,然后在叶子节点中找到目标键值。

  • 从根节点开始,逐个比较内节点内的键值。
  • 根据比较结果,选择对应的子节点并继续查找,直到到达叶子节点。
  • 在叶子节点中查找目标键。如果找到,返回对应的数据;如果未找到,则目标键不存在。

插入(Insert)

插入操作需要从叶子节点开始,并在必要时进行节点分裂(split)。

  • 从根节点开始查找应插入键的叶子节点位置。
  • 将新键插入叶子节点,并保持键的顺序排列。
  • 如果插入后叶子节点的键数超过最大限制(2t-1),则进行节点分裂:
    • 将叶子节点分裂成两个新节点,并且将中间键值上移到父节点。
    • 如果父节点也需要分裂,则继续递归分裂,必要时一直到根节点。
  • 如果根节点分裂,则树的高度增加一层。

删除(Delete)

删除操作相对复杂,因为需要确保节点的键数在允许范围内,并且保持树的平衡。

  • 删除叶子节点中的键
    • 如果目标键在叶子节点中,直接删除该键。
    • 如果删除后叶子节点的键数少于最小限制(t-1),则尝试从邻近的兄弟节点借一个键。
    • 如果无法借键,则合并当前节点与一个兄弟节点,同时从父节点移动一个键到合并中间。
  • 删除内节点中的键
    • 如果目标键在内节点中,需要找到其直接前驱或后继键替换该键,然后在叶子节点中删除前驱或后继键。
    • 删除后,处理叶子节点可能的键数不足问题,确保树的平衡和规范。

复杂度分析

B+ 树在查找、插入和删除操作上的时间复杂度均为 O(log n)$,与 B 树相似。然而,由于 B+ 树叶子节点间的链表结构,它在处理范围查询(range queries)和顺序访问时更为高效。

B+ 树在 B 树的基础上,通过引入叶子节点的链表结构和不同的节点存储策略,提高了顺序访问和范围查询的效率。它广泛应用于数据库和文件系统中,尤其适用于需要频繁进行范围查询和大量数据存取的场景。

B+ 树简化实现

以下是一个简化版的 B+树实现,用于演示 B+树的基本原理。

class BPlusTreeNode:
def __init__(self, is_leaf=False):
self.is_leaf = is_leaf
self.keys = []
self.children = []

class BPlusTree:
def __init__(self, max_degree=4):
self.root = BPlusTreeNode(is_leaf=True)
self.max_degree = max_degree

def _find(self, node, key):
for i, item in enumerate(node.keys):
if key < item:
return node.children[i]
return node.children[-1]

def _split(self, parent, child, index):
new_child = BPlusTreeNode(is_leaf=child.is_leaf)
mid_index = len(child.keys) // 2
split_key = child.keys[mid_index]

# Splits keys and children between old and new nodes
new_child.keys = child.keys[mid_index + 1:]
child.keys = child.keys[:mid_index]

if not child.is_leaf:
new_child.children = child.children[mid_index + 1:]
child.children = child.children[:mid_index + 1]

# Insert new child to parent node
parent.children.insert(index + 1, new_child)
parent.keys.insert(index, split_key)

def insert(self, key):
root = self.root
if len(root.keys) == self.max_degree - 1:
new_root = BPlusTreeNode()
new_root.children.append(self.root)
self._split(new_root, root, 0)
self.root = new_root

self._insert_non_full(self.root, key)

def _insert_non_full(self, node, key):
if node.is_leaf:
node.keys.append(key)
node.keys.sort()
else:
child = self._find(node, key)
if len(child.keys) == self.max_degree - 1:
index = node.children.index(child)
self._split(node, child, index)
if key > node.keys[index]:
child = node.children[index + 1]
self._insert_non_full(child, key)

def search(self, key):
node = self.root
while not node.is_leaf:
node = self._find(node, key)

if key in node.keys:
return True
return False

# 测试 B+Tree 的基本功能
bpt = BPlusTree(max_degree=4)
keys = [10, 20, 5, 6, 12, 30, 7, 17]

for key in keys:
bpt.insert(key)

print("搜索 12:", bpt.search(12)) # 输出:搜索 12: True
print("搜索 100:", bpt.search(100)) # 输出:搜索 100: False

以上实现展现了 B+ 树的一些基本概念和操作细节。实际系统中的 B+ 树实现会更加复杂,为了提高性能和效率,通常涉及更多的平衡调整、优化和处理机制。

关键点解释

  • 查找操作(Search):查找从根节点开始。在每个节点中,比较键值并决定向哪个子节点移动。直至到达叶子节点,然后在叶子节点中进行最终查找。
  • 插入操作(Insert)
    • 查找合适的叶子节点:查找到应该插入的叶子节点。
    • 插入键:在找到的叶子节点中插入键值并保持排序。
    • 节点分裂:如果叶子节点已满(达到最大度),则需要进行节点分裂,将其中间键提升到父节点。如果父节点也满,则继续分裂并递归进行。
  • 删除操作(Delete):先查找到包含待删除键的叶子节点,然后删除键值,并进行必要的调整以确保树的平衡性。如果一个节点因删除操作而变得过于稀疏,则需要进行节点合并或重新分配。

B+ 树在数据库系统和文件系统中被广泛采用,主要原因在于其在范围查询和排序操作中的高效性。通过理解 B+ 树的工作原理和实现细节,可以更好地掌握关系数据库的内部机制,从而在使用和优化数据库时更加游刃有余。

B* 树

B* 树是 B 树的一种改进版本,进一步优化了节点利用率,减少了树的高度。相较于 B 树,B* 树通过更加紧凑的节点布局和分裂策略提高了整体性能。

B* 树结构

  • 节点度数:每个节点包含至少 2t13\lceil {\frac{2t - 1}{3}} \rceil 个键,至多 2t12t - 1 个键。这意味着 B* 树在节点分裂时比 B 树更严格,节点分裂后其子节点也需要更高的键数。
  • 子节点指针:和 B 树一样,B* 树的节点也包含指向子节点的指针。但是,B* 树更加灵活地利用兄弟节点间的节点分裂和键移动,以提高空间利用率。

B* 树操作

查找(Search)

B* 树的查找操作与 B 树基本相同:

  • 从根节点开始,逐个比较节点内的键值。
  • 如果找到目标键,返回对应的节点。
  • 如果目标键小于当前节点内的某个键,则转向左子节点继续查找;若大于,则转向右子节点。
  • 如果到达叶子节点仍未找到目标键,则说明目标键不存在。

插入(Insert)

B* 树的插入操作比 B 树稍微复杂一些,因为它试图避免节点分裂尽可能地利用兄弟节点的空间:

  • 在树中找到插入键应当位于的叶子节点位置。
  • 将新键插入该叶子节点中,并保持键的有序性。
  • 如果插入后该节点的键数超过最大限制(2t12t-1 个键),则迭代检查兄弟节点:
    • 借键:如果相邻兄弟节点有空闲位置,可以从中借用一个键并移动适当的键到当前节点,从而避免分裂。
    • 分裂协调:如果相邻的两个兄弟节点都已满,则在父节点中插入新的键并进行三分裂(即,当前节点和两个兄弟节点重新分配键之后,分裂为三个节点)。
  • 若父节点也超过了键数限制,则对父节点进行类似的处理,必要时一直递归到根节点。

删除(Delete)

B* 树的删除操作类似于 B 树,同时考虑到需要保持较高的节点利用率:

  • 如果目标键在叶子节点中,直接删除该键。
  • 如果目标键在内部节点中:
    • 替换:用目标键的前驱或后继替换目标键,并删除前驱或后继节点中的键。
  • 删除后,如果某个节点的键数小于最小限制(2t13\lceil{\frac{2t-1}{3}}\rceil 个键),检查兄弟节点:
    • 借键:如果兄弟节点有多于最小限制的键,从兄弟节点借用一个键。
    • 合并:如果所有兄弟节点都处于最小限制,则进行节点合并,同时从父节点移动一个键到合并中间。

复杂度分析

B* 树在查找、插入和删除操作的最坏时间复杂度与 B 树相同,为 O(logn)O(\log n)。但是,由于 B* 树更高的节点利用率和减少的高度,在实践中通常表现出更好的性能,特别是对于大规模数据存储。

B* 树通过更严格的键数限制和优化的分裂与合并策略,提高了整体节点的利用率和效率,相比 B 树有更好的实用性能,尤其在需要处理大量读写操作的数据库和文件系统中表现尤为突出。

B* 树简化实现

class BSTreeNode(BPlusTreeNode):
pass # For BSTree, the logic extends from BPlusTreeNode

class BSTree(BPlusTree):
def _split(self, parent, child, index):
new_child = BSTreeNode(is_leaf=child.is_leaf)
if len(child.keys) > ((self.max_degree - 1) * 2) // 3:
mid_index = len(child.keys) // 2
split_key = child.keys[mid_index]
new_child.keys = child.keys[mid_index + 1:]
child.keys = child.keys[:mid_index]
if not child.is_leaf:
new_child.children = child.children[mid_index + 1:]
child.children = child.children[:mid_index + 1]
parent.children.insert(index + 1, new_child)
parent.keys.insert(index, split_key)
else:
# Redistribution logic
pass # Placeholder for complex redistribution logic

# 简单示例(具体完整实现需更多细节)
bst = BSTree(max_degree=4)
keys = [10, 20, 5, 6, 12, 30, 7, 17]

for key in keys:
bst.insert(key)

print("搜索 12:", bst.search(12)) # 输出:搜索 12: True
print("搜索 100:", bst.search(100)) # 输出:搜索 100: False

随着数据量的倍速增长和硬件环境的演变,索引技术也在不断发展:

  • 自适应索引:数据库系统自动选择和调整索引类型,以适应不断变化的查询模式和数据分布。
  • 分布式索引:针对大规模分布式数据库(如 Spanner、CockroachDB),需要高效的分布式索引机制,以支持全球范围内的数据一致性和快速查询。
  • 混合存储模式:行存储和列存储的混合模式,更好地支持混合负载(OLTP + OLAP)。
  • 基于机器学习的优化:利用机器学习技术,自动优化查询执行计划和索引选择,提高整体系统性能。

结语

通过上述关于 B 树、B+ 树、B* 树和 LSM 树的探讨和实现,我们可以了解到数据库传统索引技术的核心思想、演进逻辑以及未来的发展方向,这些可以为我们的数据库开发和应用提供指引。未来数据库技术的发展将继续推动其应用生态的优化和创新,为未来海量数据的智能化处理提供更高效的解决方案。


PS:感谢每一位志同道合者的阅读,欢迎关注、点赞、评论!