开发者
资源
openGauss入门(四):索引系统——B-tree、UB-tree与Psort
openGauss入门(四):索引系统——B-tree、UB-tree与Psort
原创
发表于06/23
2930

前几期聊了存储引擎和事务并发控制,这一期进入索引模块。索引是数据库查询优化的核心依赖,openGauss在索引上的设计既继承了PostgreSQL的成熟实现,又针对自己的存储引擎做了针对性扩展。行存表(Astore)默认使用B-tree索引,行存表(Ustore)默认使用UB-tree索引,列存表默认使用Psort索引。这三类索引的实现思路差异很大,逐一拆开来看。

一、B-tree:PostgreSQL遗产与openGauss的改进

openGauss的B-tree索引在整体架构上继承自PostgreSQL,但在并发控制策略上做了调整。

1.1 基础结构

B-tree索引是平衡多叉树,索引键值有序存储,每个索引元组包含索引键值和对应的TID(堆表元组物理位置)。非叶子节点用于路由搜索,叶子节点存储实际的索引条目。每个叶子节点通过right link pointer指向右侧兄弟节点,形成双向链表。

Meta page是B-tree索引的第一个页面,记录根节点的位置和fast root(当前有效的根节点)。

1.2 并发控制:从L&Y理论到工程实现

openGauss的B-tree并发控制依据Lehman & Yao的经典论文《Efficient Locking for Concurrent Operations on B-trees》,但在具体实现上与理论存在偏差。

L&Y理论的核心思路是读操作完全不加锁,节点分裂时通过link pointer保证搜索路径的正确性。具体来说,当一个节点分裂成左右两个节点后,左侧节点与分裂前的原节点占用相同的物理页,并通过link pointer与右侧新节点相连。查找过程中如果发现当前Key超出了当前页面的最大Key,说明发生了分裂,沿着link pointer向右继续查找即可。

但openGauss(以及PostgreSQL)在实际实现中,内存中的buffer是共享的,读操作必须对节点加读锁,保证读期间没有并发的写修改。这是一个务实的工程决策——理论模型假设节点在内存中的拷贝是非共享的,但实际系统中buffer pool是全局共享的,不加锁无法保证数据一致性。

写操作方面,插入流程需要先找到插入位置(从根向下搜索),记录搜索路径。在叶子节点插入时可能触发页面分裂,分裂后需要回溯更新父节点。回溯过程中,如果父节点也满了,需要继续向上分裂。实际操作中需要加锁的顺序经过优化以避免死锁。

一个值得注意的实现细节:openGauss为支持逆向扫描(从右向左),每个节点增加了left link ptr。L&Y理论中只有right link ptr,逆向扫描需要额外处理。这个改动虽然提升了逆向扫描的便利性,但也带来了额外的锁管理复杂度——分裂时除了锁定分裂节点本身,还需要锁定其右兄弟节点以修改left link ptr。

1.3 B-tree的局限

Astore的B-tree索引不存储事务信息(t_xmin/t_xmax等),因此索引元组的可见性判断无法在索引层完成。Index Scan需要先通过索引找到TID,再回表读取堆元组,然后基于堆元组头部的xmin/xmax和CSN快照做可见性判断。IndexOnly Scan场景下,如果索引不包含所有需要的列,同样需要回表。

这意味着B-tree索引本身不感知MVCC,索引中可能包含大量对当前事务不可见的死元组。这些死元组只能通过VACUUM清理,且清理时需要回表确认元组是否真的死亡。

二、UB-tree:专为Ustore打造的多版本索引

UB-tree(多版本B-tree)是openGauss为Ustore存储引擎专门开发的索引变种,在B-tree基础上增加了事务信息。UB-tree本质上是一个B-link Tree,在每一层节点间维护双向指针。

2.1 核心数据结构

UB-tree的叶子页面结构与B-tree类似,但每个索引元组尾部附加了xmin和xmax字段。xmin记录索引元组的创建事务ID,xmax记录删除事务ID。索引元组按key + TID的顺序排列,索引列相同的元组按TID作为第二关键字排序。

页面头部增加了UBTPageOpaqueDataInternal结构,包含以下几个关键字段:

  • last_delete_xid:记录页面上最后一次删除操作的事务ID
  • xid_base:页面上的事务ID基准值,页面上存储的XID是相对于xid_base的32位偏移,节省存储空间
  • activeTupleCount:页面上活跃元组的计数

其中last_delete_xid和activeTupleCount配合实现索引的自治式空间回收。

2.2 RCR UB-tree:行级多版本

RCR(Row Consistency Read)UB-tree采用行级多版本管理,将XID记录在索引元组上。最新版本和历史版本都在B-tree上,索引不记录Undo信息。

可见性判断通过比较索引元组上的xmin/xmax与快照CSN来完成,与Astore堆表元组的可见性判断逻辑类似。这使得Index Scan可以在索引层直接过滤掉不可见的元组,减少不必要的回表。

操作层面:

  • Insert:直接获取事务信息填入xmin字段
  • Delete:设置xmax字段,同时更新activeTupleCount。如果activeTupleCount减为0,尝试回收该页面
  • Update:分两种情况。非索引列更新时,索引不发生任何变化,堆表更新通过Undo记录历史版本。索引列更新时,UB-tree插入新的索引元组,指向同一个堆元组的新版本

索引分裂时,多版本信息随Key的迁移一起迁移到新页面。

2.3 PCR UB-tree:页面级多版本

PCR(Page Consistency Read)UB-tree是RCR的变种,多版本管理基于页面而非行。创建索引时通过WITH选项index_txntype=pcr或GUC参数index_txntype=pcr指定。

PCR的核心差异在于可见性判断机制:通过将页面回滚到快照可见的时刻,得到一个所有元组全部可见的页面。这相当于在页面层面做MVCC,而非逐行判断。

PCR UB-tree的Insert操作与RCR基本一致,区别在于插入前需要先申请TD(Transaction Directory)并写入Undo。PCR版本当前不支持在线创建索引、全局二级索引、极致RTO回放和备机读等功能。

2.4 自治式空间回收

UB-tree最值得关注的特性是不依赖VACUUM的独立垃圾回收机制。通过索引元组上的xmin/xmax和页面上的activeTupleCount,UB-tree能够自主判断哪些索引元组已经死亡(对当前及未来的所有事务均不可见),并在页面级别回收空间。

这个机制与Ustore的Undo空间回收配合,使得Ustore+UB-tree的组合在高更新负载下能够保持稳定的空间占用,不需要像Astore+B-tree那样依赖周期性的VACUUM来回收空间。

init_td参数控制页面支持的并发事务数,默认值为4,即一个页面最多支持4个并发事务同时修改。fillfactor默认92%,预留8%空间用于页面更新和TD列表扩展。

2.5 唯一索引支持

目前只有B-tree和UB-tree支持唯一索引。UB-tree的唯一性约束检测在索引层完成,利用索引元组上的事务信息判断是否存在对当前事务可见的重复键值。

三、Psort:列存表的默认索引

Psort(Partial Sort,局部排序聚簇索引)是列存表(Cstore)的默认索引类型。

3.1 索引即表

Psort索引本身是一个列存表。这个“索引表”的字段包括索引键中的各个列,再加上对应的行号(TID)字段。

索引表在索引列上做局部排序。所谓“局部排序”,是指数据按索引列排序后分块存储,每个CU内部有序,CU之间也保持有序。这种排序粒度比全排序轻量,但足以支持Min/Max块过滤。

3.2 查询加速机制

查询使用Psort索引时,扫描器利用每个CU记录的Min/Max值进行块过滤。对于范围查询或等值查询,只有Min/Max覆盖目标值的CU才需要被读取和解压,其他CU直接跳过。

由于索引表本身是列存格式,扫描索引列时只需要读取对应的CU,不需要读取整行,IO效率高。

3.3 与Cstore的集成

Psort索引与Cstore表的DML操作配合方式如下:数据插入时,同时写入主表和Psort索引表。由于Psort索引本身是列存表,插入需要批量进行以达到最佳效率,单条插入会先进入Delta表缓冲。

UPDATE和DELETE操作不会直接作用到Psort索引。索引的可见性通过CUDESC表中的记录来控制——当主表某行被删除或更新时,对应的CUDESC记录被标记为删除,Psort索引中对应的条目随之失效。

Psort索引不支持创建唯一索引。列存表的B-tree索引虽然支持唯一索引,但不支持表达式索引和部分索引。

3.4 性能调优参数

psort_work_mem参数控制列存表进行局部排序时,在写入临时磁盘文件之前使用的内存大小。创建索引、带partial cluster key的表插入等操作都会用到这个内存。合理配置该参数可以避免局部排序时频繁写临时文件,提升索引创建和数据加载的性能。

四、三类索引的对比与选型

维度B-tree(Astore)UB-tree(Ustore)Psort(Cstore)
存储引擎AstoreUstoreCstore
事务信息有(xmin/xmax)通过CUDESC间接管理
索引层可见性不支持支持支持(通过CUDESC)
空间回收依赖VACUUM自治式回收CU淘汰+CUDESC清理
唯一索引支持支持不支持
适用场景通用OLTP高更新OLTPOLAP/分析查询

选择建议不复杂:Astore表用B-tree,Ustore表用UB-tree(默认即如此)。列存表默认用Psort,如果有点查询需求可以额外创建B-tree索引。

UB-tree相比B-tree的核心优势在于索引层MVCC和自治回收,代价是索引元组增加了xmin/xmax字段,索引会有5%~20%左右的膨胀。这个膨胀在可接受范围内,换来的是高更新场景下索引空间的可控性和查询性能的稳定性。

收藏举报
Level 1
0
帖子
0
粉丝
0
获赞