数据库系统中普遍都会对数据进行不同程度的压缩来减小存储成本,一些面向 AP 的列存数据库的按列编码压缩还能够提高某些查询的性能。但对于大部分压缩算法,越高的压缩率也就意味着更复杂的计算和更慢的压缩/解压速度。在传统的 B 树存储结构下的数据库中,数据压缩可能会给数据写入带来 CPU 的计算压力,影响写入性能。但 OceanBase 数据库的 LSM-Tree 架构使数据的压缩只发生在 Compaction 阶段,不会影响数据的写入,同时也使能够使用压缩率更高的压缩方法,在一些客户的应用场景中也证明了 OceanBase 数据库在压缩能力上的优势。
在 OceanBase 数据库中,当 MEMTable 占用内存空间大小达到一定阈值或每日合并时会触发转储/合并,MEMTable 中的数据落盘并合并成静态的 SSTable 数据。相对于 MEMTable,SSTable 中的数据量会更大,冷数据也相对更多。在合并不断产生新的 SSTable的过程中 OceanBase 数据库 会对 SSTable 中的数据进行压缩和编码,来节省数据在硬盘上的存储空间,同时减小对 SSTable 进行查询时产生的 IO。在 SSTable 中,数据是以块为单位来组织的,2Mb 定长的宏块方便对存储空间进行管理,宏块内部变长的微块则方便我们对数据进行压缩。微块有两种存储格式,flat 微块(未编码)和 encoding 微块(编码),OceanBase 数据库的数据压缩与编码都是在微块的粒度上进行的。对于一个 encoding 格式的微块,需要经历 填充行 > 编码 > 通用压缩(可选)> 加密(可选)的流程后完成构建,成为最后落盘的数据块,并写入到定长的宏块中。这个流程中的编码和通用压缩就是 OceanBase 数据库对数据进行压缩的两种方式。
通用压缩
通用压缩指的是压缩算法对数据内部的结构没有了解的情况下,直接对数据块进行压缩。这种压缩往往是根据二进制数据的特征进行编码来减少存储数据的冗余,并且压缩后的数据不能随机访问,压缩和解压都要以整个数据块为单位进行。对数据块 OceanBase 数据库支持 zlib、snappy、lz4 和 zstd 四种压缩算法。zstd、lz4 的压缩等级是 1,zlib 压缩等级为 6,snappy 使用默认压缩等级。在 OceanBase 数据库内部对默认 16kb 大小的微块进行压缩的测试中,snappy 和 lz4 压缩速度都比较快,但压缩率比较低,zlib 和 zstd 压缩率比较高但是压缩速度要更慢一些。lz4 和 snappy 的压缩率相似但 lz4 压缩解压的速度会更快一些,同样 zstd 压缩率与 zlib 相似但压缩解压速度都更快。在 MySQL 模式下,支持用户指定单独选择上述的压缩算法,在 Oracle 模式下,兼容 Oracle 的压缩选项,只支持用户选择 lz4 或 zstd 压缩算法。
数据编码(Encoding)
在通用压缩的基础上,OceanBase 数据库自研了一套对数据库进行行列混存编码的压缩方法(encoding)。和通用压缩不同,encoding 的基础建立在压缩算法感知数据块内部数据的格式和语义的基础上。OceanBase 数据库是一个关系型数据库,数据是以表的形式来组织的,表中的每一列都有固定的类型,这就保证了同一列数据在逻辑上存在着一定的相似性;而且在一些场景下,业务的表中相邻的行之间数据也可能会更相似,所以如果将数据按列进行压缩并存储在一起就可以带来更好的压缩效果。因此,OceanBase 数据库引入了一种 encoding 格式的微块,与所有数据逐行序列化到块中的 flat 格式的微块不同,encoding 格式的微块是行列混存的,逻辑上仍然是一组行的数据存在微块里,但微块会按列对数据进行编码,编码后的定长数据存储在微块内部的列存区,部分变长数据还是按行存储在变长区。而且在 encoding 微块中,数据是可以随机访问的,当需要读微块中的一行数据时,可以只对这一行数据进行解码,避免了部分解压算法读一部分数据要解压整个数据块的计算放大;在向量化执行的过程中也可以对指定的列进行解码,降低了投影的开销。
OceanBase 数据库提供了多种按列进行压缩的编码格式,包括列存数据库中常见的字典编码,游程编码(Run-Length Encoding),整形差值编码(Delta Encoding)等。当存储的列是一个定长的数值,如 timestamp、bigint 等时,且这个微块中的数据都分布在一个值域内,整形差值编码会带来比较好的压缩效果,通过只存储每一行的值与微块中最小值的差值,然后做 bit-packing 来减少实际存储的数据量。当微块内的数据的基数(Cardinality)比较小时,字典编码和RLE编码能通过在微块内部构建字典,存储每行的引用来进行压缩。更极端的情况下,一个微块内的一列可能基本都是相同的数据,这时 OceanBase 数据库会通过常量编码(Const)只存储常量和微块内不等于常量的值,进一步提高压缩率。
除了这些比较常见的编码外,OceanBase 数据库还对字符串设计了一些编码格式。包括当一列数据中有着相似的前缀时使用前缀编码(prefix encoding),存储前缀和每行的尾缀。当一列数据是定长的字符串,并且其中几个字节相同时,可以使用定长字符串差值编码(string diff encoding),存储模式串和每行的差值数据。当微块中一列字符串数据的字符基数小于 16 时,可以使用一个十六进制数来表示这个字符,进行十六进制编码(Hex encoding)。这些字符串相关的编码对于较长的业务 ID,带格式的字符串数据等有着很好的压缩效果。
在业务存储的表中,除了同一列数据之间存在相似性,不同列的数据之间也可能有一定的关系。因此ob引入了列间编码(span-column Encoding),当两列数据大部分相同时,使用列间等值编码(Column equal encoding),这样一整列都是另外一列的引用。当一列数据是另外一列数据的前缀时,也可以使用列间子串编码(Column prefix encoding),只存储完整的一列和一列的后缀。这种列间编码可以降低在数据表设计上导致的一些数据冗余,对于一些重复的时间戳,复合列等有比较好的压缩效果,能够整体提高宏块的压缩率。但是这种列间编码在编码和解码时都会更复杂,编码时需要对不同列数据是否符合编码规则进行探测,解码时需要根据引用访问被引用的列的数据,进行处理后解码出数据,相对于其他的编码对cpu不友好一些。同时在有些情况下可能出现不同列之间可以级联引用的情况,对这中情况需要进行特殊处理。
在每列自己编码的基础上,OceanBase 数据库还支持对一列数据采用多种编码方式进行压缩,比如 hex 编码可以与其他的字符串编码叠加,但相应地也会让编码解码变得更复杂。对于 null 值的存储,不同的编码方式,列存行存都有有一些不同,但大多数都使用了一个 null bitmap,来表示该列对应行的数据是不是 null。OceanBase 数据库支持的编码格式不仅与表的 schema 相关,同时还与在一个微块内的值域等数据本身的特征相关,这也就意味着比较难以通过 DBA 设计表数据模型时指定列编码来实现较好的压缩效果,所以 OceanBase 数据库支持在对合并过程中自适应地探测更合适的编码方式来对数据进行编码来达到更高的压缩率。对 n 列数据 m 种编码进行探测理论上需要 m*n 次编码才能发现对每一列最优的编码方式,引入列间编码后又会变得更复杂,因此在编码选择算法上,OceanBase 数据库也进行了一些优化,来提高合并时数据编码的效率。
在 V3.2 版本后,encoding 又支持了向量化执行和 filter 下压,能够在编码后的数据上根据编码的特征进行过滤,降低了一些 overhead,对于一些编码也能够提高过滤效率,对部分列存的定长数据,也支持使用 AVX2 指令集进行 simd 的加速过滤。同时对于一些微块内部按列存存储的数据,向量化执行中直接按列来进行解码对 cache 和分支预测也都更加友好。
当然,除了这些优势外,encoding 也会带来一些问题,其中最直接的是编码解码带来的额外开销,包括合并过程中带来的 CPU 计算压力,查询逐行迭代时复杂的解码带来的 CPU 开销和解码器本身的开销等。OceanBase 数据库在这些问题上进行了一些优化,包括将解码器和对应的数据一起 cache 在内存中等。但是对于一些复杂的编码格式,解码本身带来的额外性能开销也是无法避免的。OceanBase 数据库也会一直尝试在查询性能,内存占用,存储成本上进行更多的权衡。
修改压缩选项
OceanBase 数据库支持通过 DDL 来对表级别的压缩/编码方式来进行配置。但对已经生成了 SSTable 的表进行压缩选项的变更时,为了避免给一次合并带来太大的 IO 写入压力,需要通过渐进合并的方式逐渐重写全部的微块数据,来完成压缩选项的变更。渐进合并的轮次可以通过表级配置项 progressive_merge_num 来设置。
建表时指定压缩选项
- MySQL 模式:
create table xxx row_format = $value compression = $value;- Oracle 模式:
create table xxx $value;修改一个表的压缩选项
- MySQL 模式:
alter table xxx [set] row_format = $value compression = $value;- Oracle 模式:
alter table xxx [move] $value;
MySQL 模式中 compression 的选项取值如下:
none
lz4_1.0
snappy_1.0
zlib_1.0
zstd_1.0
zstd_1.3.8
lz4_1.9.1
MySQL 模式中 row_format 的选项取值如下表所示。
| 值 | 微块格式 |
|---|---|
| redundant | flat |
| compact | flat |
| dynamic | encoding,encoding 支持 RAW、DICT、RLE、CONST、INTEGER_BASE_DIFF、STRING_DIFF、HEX_PACKING、STRING_PREFIX、COLUMN_EQUAL、COLUMN_SUBSTR 等编码,并且会做 bit packing |
| compressed | encoding,encoding 支持 RAW、DICT、RLE、CONST、INTEGER_BASE_DIFF、STRING_DIFF、HEX_PACKING、STRING_PREFIX、COLUMN_EQUAL、COLUMN_SUBSTR 等编码,并且会做 bit packing |
| condensed | selective encoding,encoding 的子集,selective encoding 只支持 RAW、DICT、CONST 编码,并仅对数据进行 byte packing |
| default | 等价 dynamic |
Oracle 模式中的取值及其说明如下表所示。
| 值 | 通用压缩 | 微块格式 |
|---|---|---|
| nocompress | none | flat |
| compress basic | lz4_1.0 | flat |
| compress for oltp | zstd_1.3.8 | flat |
| compress for query | lz4_1.0 | encoding |
| compress for archive | zstd_1.3.8 | encoding |
| compress for query low | lz4_1.0 | selective encoding (encoding 的子集,只使用查询更友好的编码方式) |




