非聚簇索引的原理
非聚簇索引(Non-Clustered Index)是一种索引结构,索引与数据的物理存储分离。非聚簇索引的叶子节点不直接包含表中的数据行,而是包含指向数据行的引用(如行号或主键值)。使用非聚簇索引进行查询时,首先通过索引定位到数据引用,再通过该引用去表中读取实际数据。
这意味着,一个表可以有多个非聚簇索引,每个非聚簇索引按不同的列来排序并建立索引,而数据的物理存储顺序不受影响。
实现方法
-
B+ 树结构
非聚簇索引通常采用 B+ 树结构实现。B+ 树的内部节点存储索引键,叶子节点存储的是指向实际数据行的指针(而不是数据本身)。这样可以保持高效的查询性能。 -
索引指针
在非聚簇索引的叶子节点中,除了索引键外,通常存储的是指向实际数据行的引用。这个引用可以是数据表的主键(如果有聚簇索引),也可以是存储位置(如行号、页号等)。 -
创建多个索引
非聚簇索引可以在多个列上创建,允许对不同的查询条件加速。比如,可以在一张员工表中为name
、age
、city
等字段创建非聚簇索引,分别用于按姓名、年龄和城市进行查询的优化。
为了实现一个基于 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_children=4):
self.root = BPlusTreeNode(is_leaf=True)
self.max_children = max_children
def insert(self, key, data_pointer):
"""插入操作,处理根节点的特殊情况"""
root = self.root
if len(root.keys) == self.max_children - 1:
# 如果根节点满了,进行分裂
new_root = BPlusTreeNode()
new_root.children.append(self.root)
self.split_child(new_root, 0)
self.root = new_root
self.insert_non_full(self.root, key, data_pointer)
def insert_non_full(self, node, key, data_pointer):
"""在非满节点中插入数据"""
if node.is_leaf:
# 插入到叶子节点中
self.insert_into_leaf(node, key, data_pointer)
else:
# 找到合适的子节点进行递归插入
i = len(node.keys) - 1
while i >= 0 and key < node.keys[i]:
i -= 1
i += 1
if len(node.children[i].keys) == self.max_children - 1:
self.split_child(node, i)
if key > node.keys[i]:
i += 1
self.insert_non_full(node.children[i], key, data_pointer)
def insert_into_leaf(self, leaf, key, data_pointer):
"""插入到叶子节点,保持有序"""
i = len(leaf.keys) - 1
while i >= 0 and key < leaf.keys[i]:
i -= 1
leaf.keys.insert(i + 1, key)
leaf.children.insert(i + 1, data_pointer)
def split_child(self, parent, index):
"""分裂满的子节点"""
node = parent.children[index]
mid_index = len(node.keys) // 2
mid_key = node.keys[mid_index]
# 创建新节点并复制后半部分的数据
new_node = BPlusTreeNode(is_leaf=node.is_leaf)
new_node.keys = node.keys[mid_index + 1:]
new_node.children = node.children[mid_index + 1:]
# 保留原节点前半部分数据
node.keys = node.keys[:mid_index]
node.children = node.children[:mid_index + 1]
# 父节点中插入分裂的中间键
parent.keys.insert(index, mid_key)
parent.children.insert(index + 1, new_node)
def search(self, key, node=None):
"""根据主键查找数据指针"""
if node is None:
node = self.root
if node.is_leaf:
for i, item in enumerate(node.keys):
if item == key:
return node.children[i] # 返回数据的指针
return None # 未找到
# 在内部节点,找到合适的子节点递归查找
i = len(node.keys) - 1
while i >= 0 and key < node.keys[i]:
i -= 1
return self.search(key, node.children[i + 1])
def range_query(self, start_key, end_key, node=None, result=None):
"""范围查询,返回start_key到end_key之间的所有数据指针"""
if node is None:
node = self.root
if result is None:
result = []
if node.is_leaf:
# 在叶子节点中找到范围内的数据指针
for i, key in enumerate(node.keys):
if start_key <= key <= end_key:
result.append(node.children[i])
else:
# 在内部节点,递归找到适合的子节点
for i, key in enumerate(node.keys):
if key >= start_key:
self.range_query(start_key, end_key, node.children[i], result)
if key > end_key:
break
if node.keys and node.keys[-1] < end_key:
self.range_query(start_key, end_key, node.children[-1], result)
return result
# 数据表模拟,表中数据存储在字典中,索引指针为数据的主键
class DataTable:
def __init__(self):
self.data = {} # 用来存储数据,key 为主键,value 为具体数据
self.index = BPlusTree() # 非聚簇索引基于B+树
def insert(self, primary_key, row_data):
"""在表中插入数据,并在非聚簇索引中建立索引"""
if primary_key in self.data:
print(f"主键 {primary_key} 已存在,无法插入重复数据。")
else:
self.data[primary_key] = row_data
self.index.insert(primary_key, primary_key) # 索引存储主键作为指针
def query_by_index(self, index_key):
"""根据非聚簇索引查询数据"""
primary_key = self.index.search(index_key)
if primary_key is not None:
return self.data[primary_key]
else:
return "记录不存在"
def range_query_by_index(self, start_key, end_key):
"""根据非聚簇索引进行范围查询"""
primary_keys = self.index.range_query(start_key, end_key)
return [self.data[pk] for pk in primary_keys]
# 测试数据表和非聚簇索引
table = DataTable()
# 插入数据
table.insert(1001, {"name": "Alice", "age": 30, "city": "New York"})
table.insert(1003, {"name": "Bob", "age": 24, "city": "Los Angeles"})
table.insert(1002, {"name": "Charlie", "age": 29, "city": "Chicago"})
table.insert(1005, {"name": "David", "age": 35, "city": "Houston"})
table.insert(1004, {"name": "Eve", "age": 22, "city": "Miami"})
# 查询单个数据
print("\n查询主键为1002的记录:")
print(table.query_by_index(1002))
# 范围查询
print("\n查询主键在1002到1004之间的记录:")
print(table.range_query_by_index(1002, 1004))
输出结果:
查询主键为1002的记录:
{'name': 'Charlie', 'age': 29, 'city': 'Chicago'}
查询主键在1002到1004之间的记录:
[{'name': 'Charlie', 'age': 29, 'city': 'Chicago'}, {'name': 'Eve', 'age': 22, 'city': 'Miami'}]
代码解析:
-
B+ 树结构:
BPlusTreeNode
表示 B+ 树的节点,分为叶子节点和内部节点。叶子节点存储键值对及其数据指针,内部节点存储索引键和子节点引用。- B+ 树通过
insert
方法将索引键插入树中,叶子节点存储主键作为指针。
-
数据表结构:
DataTable
模拟实际的数据表,数据存储在字典中,主键作为数据的标识。- 在插入数据时,同时将主键作为索引指针插入 B+ 树索引中,允许通过索引快速找到数据的主键,并通过主键在数据表中获取实际数据。
-
查询与范围查询:
query_by_index
方法通过 B+ 树索引找到对应的主键,再根据主键获取实际数据。range_query_by_index
方法通过 B+ 树进行范围查询,返回符合条件的主键列表,然后从数据表中取出数据。
总结:
该实现展示了如何用 B+ 树结构和索引指针来构建非聚簇索引,查询时通过索引先获取指向数据的引用,再根据引用获取实际数据。这样模拟了实际数据库中的非聚簇索引查询逻辑。
应用场景
-
频繁按非主键字段查询
当查询经常基于非主键列(如姓名、地址等),而不只是按主键进行查询时,非聚簇索引非常适合。它允许快速通过非主键列查找数据行的引用,然后根据引用查找实际数据。 -
多列查询加速
如果一个表经常基于多个不同列进行查询,比如有时根据年龄查询,有时根据城市查询,每个列都可以建立一个非聚簇索引。这样,每种查询都能利用相应的索引加速。 -
不改变数据存储顺序
非聚簇索引不会改变数据的物理存储顺序,这在一些场景下非常重要,尤其是当数据表有聚簇索引时,非聚簇索引只是在原有的物理存储顺序上进行额外的优化。 -
少量数据更新的表
非聚簇索引的维护成本高于聚簇索引,因为每次插入、删除、更新数据时,索引也需要更新。因此,适合那些数据更新较少、查询较多的场景。
优缺点
优点:
- 灵活性强:可以在多个列上创建非聚簇索引,满足各种查询需求。
- 不改变数据物理顺序:不会影响数据在磁盘上的物理存储方式,适合大多数应用。
- 查询加速:在某些特定列上的查询性能显著提升,特别是那些涉及筛选、排序或聚合操作的查询。
缺点:
- 插入/更新成本高:数据的插入、删除或更新操作会导致非聚簇索引的调整和维护,性能会受到影响。
- 占用额外空间:非聚簇索引需要额外的存储空间来保存索引结构,尤其当多个索引存在时。
- 二次查找:由于非聚簇索引的叶子节点存储的是数据引用,因此在查询时通常需要额外的“查找跳转”步骤。
应用示例
假设我们有一张包含员工信息的表,包含以下字段:id
(主键),name
,age
,city
。如果我们经常需要按 city
进行查询,可以为 city
列创建非聚簇索引。
CREATE NONCLUSTERED INDEX idx_employee_city
ON employees(city);
这样,当我们执行以下查询时:
SELECT * FROM employees WHERE city = 'New York';
数据库系统会首先通过 city
列的非聚簇索引查找到符合条件的行号或主键,然后再去表中获取实际的数据行。这大大加快了查询速度。
总结
- 非聚簇索引 提供了一种灵活的索引机制,可以在不改变数据物理存储顺序的情况下,对多个列进行索引优化,提升查询性能。
- 它适合那些频繁进行查询、但数据更新较少的场景,并且在需要多列查询优化时尤为有用。
- 虽然非聚簇索引有额外的维护成本和存储开销,但它的灵活性和对查询性能的提升,使其在许多数据库应用中得到了广泛使用。
产品简介
- 梧桐数据库(WuTongDB)是基于 Apache HAWQ 打造的一款分布式 OLAP 数据库。产品通过存算分离架构提供高可用、高可靠、高扩展能力,实现了向量化计算引擎提供极速数据分析能力,通过多异构存储关联查询实现湖仓融合能力,可以帮助企业用户轻松构建核心数仓和湖仓一体数据平台。
- 2023年6月,梧桐数据库(WuTongDB)产品通过信通院可信数据库分布式分析型数据库基础能力测评,在基础能力、运维能力、兼容性、安全性、高可用、高扩展方面获得认可。
点击访问:
梧桐数据库(WuTongDB)相关文章
梧桐数据库(WuTongDB)产品宣传材料
梧桐数据库(WuTongDB)百科