目录
- openGauss数据库SQL引擎
- openGauss数据库执行器技术
- openGauss存储技术
一、openGauss存储概览
二、openGauss行存储引擎
三、openGauss列存储引擎
Ⅰ、列存储引擎的总体架构
Ⅱ、列存储的页面组织结构
Ⅲ、列存储的MVCC设计
IV、列存储的索引设计
V、列存储自适应压缩
VI、列存储的持久化设计
四、openGauss内存引擎
- openGauss事务机制
- openGauss数据库安全
openGauss存储技术
三.openGauss列存储引擎
传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。在分析性的作业以及业务负载的情况下,数据库往往会遇到针对大量表的复杂查询,而这种复杂查询中往往仅涉及一个较宽(表列数较多)的表中个别列。此类场景下,行存储以行作为操作单位,会引入与业务目标数据无关的数据列的读取与缓存,造成了大量IO的浪费,性能较差。因此openGauss提供了列存储引擎的相关功能。创建表的时候,可以指定行存储还是列存储。
总体来说,列存储有以下优势:
(1) 列的数据特征比较相似,适合压缩,压缩比很高,在数据量较大(如数仓)场景下会节省大量磁盘空间;压缩比高同时也会提高单位作业下的IO效率。
(2) 当表中列数比较多,但是访问的列数比较少时,列存储可以按需读取列数据,大大减少不必要的读IO,提高查询性能。
(3) 基于列批量数据向量运算,结合向量化执行引擎,CPU的缓存命中率比较高,性能比较好,更适合OLAP大数据统计分析的场景。
(4) 列存储表同样支持DML操作和MVCC,功能完备,且在使用角度做了良好的兼容,基本是对用户透明的,方便使用。
列存储引擎的总体架构
01
列存引擎的存储基本单位是CU(Compression Unit,压缩单元),即表中一列的一部分数据组成的压缩数据块。行存引擎中是以行作为单位来管理,而当使用列存储时,整个表整体被按照不同列划分为若干个CU,实例如图25所示。
图25 CU划分方式
如图25所示,假设以6万行作为一个单位,则一个12万行、3列宽的表,则被划分为8个CU,每个CU对应一个列上的6万个列数据。图中有Col0、Col1、Col2、Col3四列,数据按照行切分了两个Row group(行组),每个Row group有固定的行数。针对每个Row group按照列做数据压缩,形成压缩单元CU(Compression unit)。每个Row group内部各个列的CU的行边界是完全对齐的。当然,大部分时候,CU在经过压缩后,因为数据特征与压缩率的不同,文件大小会完全不同,例如图26所示。
图26 压缩单元示意图
为了管理表对应的CU,与执行器层进行对接来提供各种功能,列存储引擎使用了CUDesc(压缩单元描述符)表来记录一个列存表中CU对应的元信息,如图27所示。
图27 列存引擎整体架构图
注:Cmn 表示第m列的cuid是n的压缩单元。
每个CU对应一个CU Desc的记录,在CU desc里记录了整个CU的事务时间戳信息、CU的大小、存储位置、magic校验码、min/max等信息。
与此同时,每张列存表还配有一张Delta表,Delta表自身为行存储表。当有少量的数据插入到一张列存表时,数据会被暂时放入Delta表,等到到达阈值或满足一定条件或操作时再行整合为CU文件。Delta表可以帮助我们避免单点数据操作带来的很重的CU操作与开销。
设计采用级别的多版本并发控制,删除通过引入Virtual Column Bitmap来标记删除。Bitmap是多版本的。
列存储的页面组织结构
02
上面讲到了CUDesc表以及其用来记录元信息的目的。CUDesc的典型结构如图28。
图28 CUDesc的典型结构
其中:
(1)_rowTupleHeader为传统行存记录的行头,其中包含了前面提到过的事务以及位置信息等,用来进行可见性判断等。
(2)cu_mode实际为此CUDesc对应CU的infomask,记录了一些CU的特征信息(比如是否Full,是否有NULL等等)
(3)magic是CUDesc与CU文件之间校验的关键信息。
(4)min/max(最小值/最大值)为稀疏索引,后续会进一步展开。
而CU文件本身结构,则如图29所示。
图29 CU文件本身结构
列存储在CUDesc表的存储信息基础上设计了一套与上层交互的操作API。除了上面列存储的页面组织结构以及文件管理中天然可以展示出的结构机制之外,列存储还有一些关键的技术特征:
(1)列存储的CU中数据的删除,实际上是标记删除。删除操作,相当于是更新了CUDesc表中CU对应CUDesc记录的delete bitmap(删除位图)结构,标记列中某行对应数据已被删除,而CU文件数据不会被更改。这样可以避免删除操作带来的IO放大以及解压、压缩的高额CPU开销。这样的设计,也可以使得对于同一个CU的select(查询)和delete(删除)互不阻塞,提升并发能力。
(2)列存储CU中数据更新,则是遵循append-only(仅允许追加)原则的,即CU文件仅会向后进行延展扩充,亦或是启用新的CU文件,而不是在对应行在CU中的位置就地更新。
(3)由于CU以及CUDesc的元数据管理模式,原有系统中的Vacuum机制实际上并不会非常有效的清除CU中已经失效的存储空间,因为Lazy Vacuum(清理数据时,只是标识无用行的状态可以录入新数据,不会影响对表数据的操作)仅能在CUDesc级别进行操作,在多数场景下无法对CU文件本身进行清理。列存储内部如果要对列存数据表进行清理,需要执行Vacuum Full(除了清理无用行,还会合并数据块,整个过程会锁定表)操作。
列存储的MVCC设计
03
理解了CU、CUDesc的基本结构,以及CUDesc的管理,或者说是其“代理”角色,列存的MVCC设计以及管理,实际上就非常好理解了。
由于列存的操作基本单位CU是由CUDesc表中的行进行管理的,因此列存表的CU可见性判断,也是由CUDesc的行头信息、按照传统的行存可见性进行判断的。
同样的,列存可见性的单位,也是CU级别(CUDesc),不同于行存的Tuple级别。
列存表的并发控制是CU文件级别的,实际上也等同于其CUDesc代理表的CUDesc行之间的并发控制。多个事务之间在一个CU上的并发管控,实际上取决于在其对应的CUDesc记录上是否冲突。例如:
(1)两个事务并发去读一个CU是可进行的,两个事务都可以拿到此CU对应CUDesc行级别的share lock(共享锁)。
(2)两个事务并发去更新一个CU,会因为在CUDesc上的锁冲突而触发一个事务回滚(当然,如果是read commited(读已提交)隔离级别并打开允许并发更新的开关,这里会做的事情是拿到此CUDesc最新版本的ctid,然后重跑一部分queryTree(查询树),来进行更新操作;此部分内容,详见第7章事务相关章节)。
(3)两个事务并行执行,一个事务对一个CU执行了delete操作并先行提交,另一个事务在repeatable read(可重读)的隔离级别下,其获取的快照,只能看到这个CUDesc在delete发生前的版本,这个版本中的CUDesc中的delete_bitmap(删除位图),对应数据没有被标记删除。也由于CU的行删除是标记删除的机制,因此数据在原有CU的数据文件中依旧可用,此事务依旧可以在其对应的快照下读到对应行。
图30 删除CU中部分数据所进行的实际操作
删除CU中部分数据所进行的实际操作如图30所示。
从上面的几个例子可以看出,列存储对于更新的append only(仅允许追加)策略以及对于删除的标记删除方式,对于列存事务ACID的支持,是至关重要的。