前两期聊了openGauss的整体差异和事务并发控制,这一期进到存储引擎层。openGauss的磁盘引擎提供了三种存储格式:Astore(追加写行存储)、Ustore(原地更新行存储)和Cstore(列存储)。三者定位不同,实现思路也完全不同。这一期把三套引擎的页面布局、元组结构、多版本管理和空间回收机制逐一拆开来看。
一、Astore:追加写的行存储
Astore是openGauss的默认行存储引擎,全称Append-Store,核心思想是“追加写”——新数据尽量往页面末尾追加,不覆盖已有数据。

astore整体框架
1.1 页面与元组结构
Astore的页面大小为8KB,页面布局与PostgreSQL的heap页类似:页面头部(PageHeaderData)后面跟着一个元组指针数组(ItemId数组),每个指针指向页面内一条元组的偏移和长度。指针数组从页面头部向后生长,元组数据从页面尾部向前生长,中间是空闲空间。
元组结构是理解Astore多版本机制的关键。Astore堆表元组头部的定义如下:
几个关键字段的含义:
- t_xmin:插入这条元组的事务ID。对应系统字段序号-3。
- t_xmax:删除这条元组的事务ID。如果尚未删除则为0。对应系统字段序号-5。
- t_cid:插入或删除操作在当前事务内的命令序号。同一事务内执行多条INSERT/UPDATE/DELETE时,靠这个字段区分先后。
- t_ctid:当前元组的页面号和页内指针下标。如果元组被更新过,则指向更新后新元组的位置。
- t_infomask:标志位,记录了插入/删除事务是否已提交/回滚、是否有NULL字段、是否有变长字段等信息。
元组内容紧跟在头部之后,偏移由t_hoff指定。
1.2 多版本与可见性
Astore采用Append Update模式:UPDATE操作不会在原地修改元组,而是在页面空闲位置插入一条新元组,旧元组标记为“已删除”。具体来说:
- 新版本元组插入到页面中某个空闲位置
- 旧版本元组的t_ctid更新为指向新版本的页面号和偏移
- 旧版本元组的t_xmax设置为当前事务ID
- 新版本元组的t_xmin设置为当前事务ID
版本链通过t_ctid串联:从旧版本顺着t_ctid能找到新版本,但从新版本找不到旧版本(单向链)。查询时用快照中的CSN与元组的t_xmin/t_xmax对应的CSN比较,判断该版本是否可见。不可见则顺着t_ctid找下一个版本。
这种设计的代价很直接:频繁UPDATE会导致同一逻辑行在页面上留下多个死元组版本,页面快速膨胀。需要VACUUM来回收空间。
1.3 空间回收的三级机制
Astore的空间回收分三个级别:
轻量级清理:查询扫描页面时顺带清理。当一个查询访问某个页面时,如果发现页面内有对所有并发事务都不可见的死元组,会顺手将其标记为可复用空间。这种清理不额外消耗资源,但覆盖面有限。
中量级清理:由VACUUM命令触发。扫描表的所有页面,识别死元组并回收其占用的空间。回收后的空间可以被后续INSERT复用。
重量级清理:VACUUM FULL。不仅回收空间,还会重写整个表,消除碎片,将数据紧凑排列。代价是需要获取排他锁,阻塞所有并发读写。
autovacuum后台线程会定期自动执行中量级清理,但遇到长事务时会阻塞回收——因为长事务可能还需要看到旧版本数据。
二、Ustore:原地更新的行存储
Ustore(Undo Store)是openGauss 2.1.0引入的新行存储引擎,核心思路是“原地更新”。
2.1 设计哲学:新旧分离
Astore把新版本和旧版本混在同一个数据页面里,Ustore把最新版本留在数据页面,旧版本全部搬到独立的Undo空间。
数据页面不再堆积死元组,表空间增长曲线比Astore平滑得多。Undo空间统一分配、集中回收,复用效率更高。
2.2 核心数据结构
Ustore引入了几套新的数据结构:
TD(Transaction Directory,事务目录) :页面头部记录每个事务的ID及其对应的Undo记录指针。每个TD条目存一个xid + undo_record_ptr。页面能支持的最大并发数由init_td参数控制(默认值,可配置)。
UHeapDiskTupleData:磁盘上的元组结构,包含td_id(指向TD的索引)和xid偏移等信息。
Undo记录:存储在Undo段中,记录了修改前的完整元组数据以及指向更早版本的指针,形成Undo链。
2.3 查询与可见性判断
Ustore的可见性判断与Astore共用同一套CSN事务管理机制。差异在于数据获取路径:
- Astore:元组头部直接包含t_xmin/t_xmax,可见性判断一步到位
- Ustore:元组头部只存td_id,需要通过TD找到事务ID,再查CSNLOG判断可见性
如果当前版本不可见,Ustore沿着Undo链向前遍历,从Undo段中读出旧版本数据,重复可见性判断。
Ustore做了几层优化来避免频繁读取Undo:
- 有效TD:直接得到元组最新事务信息
- 冻结TD(FROZEN) :元组对所有事务可见,无需再查
- 无效TD:修改元组的事务已提交且比当前TD中的事务更旧,直接用TD中的事务做可见性判断即可,不需要读Undo
只有当前版本不可见且TD无效时,才真正进入Undo链遍历路径。
2.4 NUMA-Aware的Undo子系统
Ustore的Undo子系统采用NUMA-Aware设计。在多核平台上,每个NUMA节点有独立的Undo空间分配策略,减少跨节点内存访问。Undo launcher和undo worker线程协同工作,负责Undo空间的分配、回收和事务回滚。
Undo空间支持细粒度的资源控制:可按事务运行时长、单事务Undo空间大小、整体Undo空间阈值进行“监管”,超限时事务报错回滚。
2.5 索引的差异
Astore的索引不包含事务信息,索引元组指向堆表元组的物理位置(页面号+偏移)。可见性判断需要回表。
Ustore采用多版本索引技术:索引元组中携带了事务信息,可以在索引层直接判断可见性,减少回表次数。同时解决了Astore的索引膨胀问题——不需要频繁清理索引中的死元组。
三、Cstore:列存储
Cstore是面向AP场景的列存储引擎。
3.1 CU:列存储的基本单元
Cstore的存储单元是CU(Compression Unit,压缩单元)。每个CU存储表中某一列的连续若干行数据(默认MAX_BATCH_ROWS=60000行)。
CU的结构如下:
相邻CU之间按8KB对齐。
3.2 CUDesc表:元数据与可见性
每个CU对应CUDESC表(实际是一个Astore行存储表)中的一行记录。CUDESC行记录了这个CU的元数据信息,包括该列的最小值、最大值——用于查询时跳过不相关CU。
CU的可见性完全由CUDESC表中对应行的可见性决定。CU本身只支持追加写,没有独立的事务信息。DML操作(UPDATE/DELETE)在Cstore上的处理方式是:
- 旧CU对应的CUDESC行标记为删除
- 新数据写入新的CU,CUDESC表插入新行
CU间和CU内的可见性控制原理与Astore相同。
3.3 Delta表:缓解小批量写入
Cstore是为批量导入设计的,单条INSERT或小批量UPDATE会产生大量小CU,碎片化严重。
解决方案是Delta表——每个Cstore表附带一个Astore行存表作为缓冲。单条插入或小批量更新先写入Delta表,后台线程定期将Delta表中的数据迁移到主CU中。查询时同时扫描主CU和Delta表并合并结果。
3.4 向量化执行
Cstore与openGauss的向量化执行引擎天然配合。向量化引擎一次处理一批元组(而非逐行),底层扫描节点直接装填向量化的列数据。配合SIMD指令,AP查询性能显著优于行存。
四、选型建议
选择不复杂:
- OLTP业务用行存储,默认Astore。如果UPDATE频繁、对表膨胀敏感,切Ustore。
- OLAP业务用Cstore,但要求批量导入,避免高频单条DML。
- 注意:Ustore不支持列存储和段页式存储。
参考文献:cnblogs.com/openGauss-bot/articles/18165927
前两期聊了openGauss的整体差异和事务并发控制,这一期进到存储引擎层。openGauss的磁盘引擎提供了三种存储格式:Astore(追加写行存储)、Ustore(原地更新行存储)和Cstore(列存储)。三者定位不同,实现思路也完全不同。这一期把三套引擎的页面布局、元组结构、多版本管理和空间回收机制逐一拆开来看。
一、Astore:追加写的行存储
Astore是openGauss的默认行存储引擎,全称Append-Store,核心思想是“追加写”——新数据尽量往页面末尾追加,不覆盖已有数据。

astore整体框架1.1 页面与元组结构
Astore的页面大小为8KB,页面布局与PostgreSQL的heap页类似:页面头部(PageHeaderData)后面跟着一个元组指针数组(ItemId数组),每个指针指向页面内一条元组的偏移和长度。指针数组从页面头部向后生长,元组数据从页面尾部向前生长,中间是空闲空间。
元组结构是理解Astore多版本机制的关键。Astore堆表元组头部的定义如下:
typedef struct HeapTupleFields { ShortTransactionId t_xmin; /* 插入元组的事务号 */ ShortTransactionId t_xmax; /* 删除元组的事务号 */ union { CommandId t_cid; /* 插入或删除命令在事务中的命令号 */ ShortTransactionId t_xvac; } t_field3; } HeapTupleFields; typedef struct HeapTupleHeaderData { union { HeapTupleFields t_heap; DatumTupleFields t_datum; } t_choice; ItemPointerData t_ctid; /* 当前元组或更新后元组的行号 */ uint16 t_infomask2; /* 字段个数和标记位 */ uint16 t_infomask; /* 标记位 */ uint8 t_hoff; /* 元组头部大小(含NULL位图和对齐) */ bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* NULL字段位图 */ } HeapTupleHeaderData;几个关键字段的含义:
元组内容紧跟在头部之后,偏移由
t_hoff指定。1.2 多版本与可见性
Astore采用Append Update模式:UPDATE操作不会在原地修改元组,而是在页面空闲位置插入一条新元组,旧元组标记为“已删除”。具体来说:
版本链通过t_ctid串联:从旧版本顺着t_ctid能找到新版本,但从新版本找不到旧版本(单向链)。查询时用快照中的CSN与元组的t_xmin/t_xmax对应的CSN比较,判断该版本是否可见。不可见则顺着t_ctid找下一个版本。
这种设计的代价很直接:频繁UPDATE会导致同一逻辑行在页面上留下多个死元组版本,页面快速膨胀。需要VACUUM来回收空间。
1.3 空间回收的三级机制
Astore的空间回收分三个级别:
轻量级清理:查询扫描页面时顺带清理。当一个查询访问某个页面时,如果发现页面内有对所有并发事务都不可见的死元组,会顺手将其标记为可复用空间。这种清理不额外消耗资源,但覆盖面有限。
中量级清理:由VACUUM命令触发。扫描表的所有页面,识别死元组并回收其占用的空间。回收后的空间可以被后续INSERT复用。
重量级清理:VACUUM FULL。不仅回收空间,还会重写整个表,消除碎片,将数据紧凑排列。代价是需要获取排他锁,阻塞所有并发读写。
autovacuum后台线程会定期自动执行中量级清理,但遇到长事务时会阻塞回收——因为长事务可能还需要看到旧版本数据。
二、Ustore:原地更新的行存储
Ustore(Undo Store)是openGauss 2.1.0引入的新行存储引擎,核心思路是“原地更新”。
2.1 设计哲学:新旧分离
Astore把新版本和旧版本混在同一个数据页面里,Ustore把最新版本留在数据页面,旧版本全部搬到独立的Undo空间。
数据页面不再堆积死元组,表空间增长曲线比Astore平滑得多。Undo空间统一分配、集中回收,复用效率更高。
2.2 核心数据结构
Ustore引入了几套新的数据结构:
TD(Transaction Directory,事务目录) :页面头部记录每个事务的ID及其对应的Undo记录指针。每个TD条目存一个xid + undo_record_ptr。页面能支持的最大并发数由
init_td参数控制(默认值,可配置)。UHeapDiskTupleData:磁盘上的元组结构,包含td_id(指向TD的索引)和xid偏移等信息。
Undo记录:存储在Undo段中,记录了修改前的完整元组数据以及指向更早版本的指针,形成Undo链。
2.3 查询与可见性判断
Ustore的可见性判断与Astore共用同一套CSN事务管理机制。差异在于数据获取路径:
如果当前版本不可见,Ustore沿着Undo链向前遍历,从Undo段中读出旧版本数据,重复可见性判断。
Ustore做了几层优化来避免频繁读取Undo:
只有当前版本不可见且TD无效时,才真正进入Undo链遍历路径。
2.4 NUMA-Aware的Undo子系统
Ustore的Undo子系统采用NUMA-Aware设计。在多核平台上,每个NUMA节点有独立的Undo空间分配策略,减少跨节点内存访问。Undo launcher和undo worker线程协同工作,负责Undo空间的分配、回收和事务回滚。
Undo空间支持细粒度的资源控制:可按事务运行时长、单事务Undo空间大小、整体Undo空间阈值进行“监管”,超限时事务报错回滚。
2.5 索引的差异
Astore的索引不包含事务信息,索引元组指向堆表元组的物理位置(页面号+偏移)。可见性判断需要回表。
Ustore采用多版本索引技术:索引元组中携带了事务信息,可以在索引层直接判断可见性,减少回表次数。同时解决了Astore的索引膨胀问题——不需要频繁清理索引中的死元组。
三、Cstore:列存储
Cstore是面向AP场景的列存储引擎。
3.1 CU:列存储的基本单元
Cstore的存储单元是CU(Compression Unit,压缩单元)。每个CU存储表中某一列的连续若干行数据(默认MAX_BATCH_ROWS=60000行)。
CU的结构如下:
相邻CU之间按8KB对齐。
3.2 CUDesc表:元数据与可见性
每个CU对应CUDESC表(实际是一个Astore行存储表)中的一行记录。CUDESC行记录了这个CU的元数据信息,包括该列的最小值、最大值——用于查询时跳过不相关CU。
CU的可见性完全由CUDESC表中对应行的可见性决定。CU本身只支持追加写,没有独立的事务信息。DML操作(UPDATE/DELETE)在Cstore上的处理方式是:
CU间和CU内的可见性控制原理与Astore相同。
3.3 Delta表:缓解小批量写入
Cstore是为批量导入设计的,单条INSERT或小批量UPDATE会产生大量小CU,碎片化严重。
解决方案是Delta表——每个Cstore表附带一个Astore行存表作为缓冲。单条插入或小批量更新先写入Delta表,后台线程定期将Delta表中的数据迁移到主CU中。查询时同时扫描主CU和Delta表并合并结果。
3.4 向量化执行
Cstore与openGauss的向量化执行引擎天然配合。向量化引擎一次处理一批元组(而非逐行),底层扫描节点直接装填向量化的列数据。配合SIMD指令,AP查询性能显著优于行存。
四、选型建议
选择不复杂:
参考文献:cnblogs.com/openGauss-bot/articles/18165927