存储引擎原理简述


1 MARS3 概述

MARS3 存储引擎在 MARS2 基础上进行多项优化,采用行列结合的存储方式,同时实现数据的高速写入与极限压缩。

MARS3 支持通过 UPDATEDELETE 子句实现数据更新与删除(Unique Mode 模式除外)。

MARS3 支持增删列,支持 COPYpg_dump 操作。

1.1 内部原理

对于每个 MARS3 单表而言,其内部均采用 LSM Tree 结构存储。LSM Tree(Log Structured Merge Tree)是一种分层的,有序的,面向磁盘的数据结构。其核心思想是充分利用磁盘性能进行批量的顺序写操作,性能远高于随机写。

MARS3 内部原理图如下:

我们将以概念逐层剖析的形式来解读上图。

1.1.1 排序键

  • MARS3 中,数据是有序存储的。在创建表时,需要通过指定排序列(可以多列)的方式指定排序的顺序。这个排序顺序涉及的字段,称为排序键。
  • 排序键只能指定一次,不能修改,不能新增,不能删除
  • 为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。
  • 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用 COLLATE C 能够加速排序。
  • 指定排序键的 SQL 关键词:ORDER BY

1.1.2 行存和列存

  • MARS3 支持采用先行后列的存储方式存储数据,也支持直接写为列存,可通过 prefer_load_mode 参数设置加载模式,详见下文配置项
  • 如果采用行列混存的模式,则写入的数据会先以行存的形式储存下来,等数据量积累一定程度在进行列存转换。
  • 和直接把数据变成列存相比,有以下几种好处:
    • 对于高频、小批量数据来说写入速度更快
    • 不需要大量的内存进行数据缓存
    • 保证每个数据块元组数量的均匀

1.1.3 Run、增量文件和元信息

  • 根据排序键,MARS3 中存储的数据是有序的,一段连续有序的数据称为 Run
  • Run 分成两种,为了能够高速写入,插入的数据会以行存 Run 的形式储存下来,之后为了方便读取和压缩,我们会把行存 Run 转换成列存 Run。
  • 每个 Run 都有自己的增量文件,除了记录主要数据的 Data 文件之外,还有储存大数据块的 Toast 文件,储存删除信息的 Delta 文件,储存索引信息的 Index 文件,储存合并(详见下节"合并与回收")信息的 link 文件等(行存 Run 和列存 Run 的增量文件稍有不同)。
  • 同时,为了知道这些文件的所在位置,我们额外记录一些信息,称为元信息。元信息会记录文件的存储位置,文件大小,数据条数,压缩状况等信息。
  • Run(使用 rowstore_size 参数配置)的大小可以灵活的进行调节,以适应不同场景达到最优性能。

1.1.4 合并和回收

  • Run 中如果数据范围有重叠,则会造成读放大,降低查询效率。因此,当磁盘上的 Run 数量超过一定值时,MARS3 会将磁盘上的多个 Run 进行归并排序,最后输出为一个 Run。这个过程称为合并
  • 合并过程中,数据依然可读可写:
    • 读数据时,只会读合并的输入文件
    • 写数据时,合并过程不会读新写入的数据
    • 读,写,合并三者之间不会互相阻塞
  • 合并完成后,参加合并的 Run 会根据事务 ID 自动决定何时不再被需要,并且标记为可回收状态

1.1.5 Level

  • 为了让合并的输入文件尺寸相近(避免超大文件与小文件合并),Run 被组织到 Level 中,最大可有 10 层:L0,L1,L2......L9。
  • 每一层的 Run 个数达到一定数目,或者同一层多个 Run 的大小总和达到阈值都会触发合并,合成一个 Run 后升级到更高层去;并且为了加快 Run 的升级,允许同一层中同时进行多个合并任务。

1.1.6 MARS3 Brin 索引

  • MARS3 支持创建 Brin 索引,支持 Brin 索引的删除和新增
  • 每个 Run 在生成的时候都会创建自己独立的 Brin 索引文件。
  • 示例:CREATE INDEX brin_idx ON t1 USING mars3_brin(time,tag_id);

1.1.7 压缩

  • 默认情况下,MARS3 所有的数据列,采用 lz4 进行压缩。
  • 支持手动指定编码链压缩算法,可以整个表指定,也可以单个列指定。

1.1.8 支持 MVCC 机制

  • MVCC(Multiversion Concurrency Control)机制通常被称为多版本管理。它的核心是对数据的更新、修改和删除处理
  • 多版本管理中,数据的更新和删除并不一定会在原数据上进行修改,而是需要创立一个新的版本,把原数据标记为失效的数据,再在新版本上增加新数据,数据具有多个版本。每个数据带有一个版本信息,且历史版本均会被保存。
  • MARS3 的更新和删除操作都不是采用原地修改数据的方式,而是依靠 Delta 文件和版本信息屏蔽掉了老数据,从而控制数据的可见性。
  • 注意:持续进行更新或删除同一个 Run 的数据会让此 Run 的 Delta 文件占用的物理空间持续增加,但当当前 Run 的所有数据都被删除之后就不会再增加了。而且 MARS3 的合并操作可以自动清除已 Dead 的数据,当然你也可以有计划地定期使用 VACUUM 清理已 Dead 的数据。

1.1.9 数据写入

  • 数据通过 INSERT 写入到内存中,再写入 L0 的 Run 中。
  • L0 中的 Run 大小是可以配置的,详见下文配置项部分。

1.1.10 更新和删除

  • MARS3 通过 DELETE 进行删除,删除会在对应 Run 的 Delta 文件中进行记录,在进行 Run 合并的时候真正把数据删除。
  • MARS3 通过 UPDATE 进行更新,更新会先删除原本数据,再重新插入一条新数据。
  • MARS3 的 Unique Mode 模式暂时不支持删除,更新则无需显式使用 UPDATE 子句,直接执行 INSERT 子句即可自动完成操作。如果想要更新某个 Unique Key(即建表时指定的排序键所对应的具体键值)对应的某条数据,直接插入一条相同 Unique Key 的新数据即可。例如 CREATE TABLE mars3_t(c1 int NOT NULL, c2 int) USING MARS3 WITH (uniquemode=true) ORDER BY (c1, c2);,其中 Unique Key 即为 (c1, c2)

注意!
如开启 Unique Mode,则 ORDER BY 子句的第一个字段在定义时需要添加 NOT NULL 约束。

1.2 MARS3 使用

1.2.1 创建 MARS3 表

在已创建 matrixts 扩展的前提下,最简洁的建表方式,只需要在 CREATE TABLE 语句加上 USING 子句,并附加 ORDER BY 子句。延伸示例请见表设计最佳实践

=# CREATE TABLE metrics (
    ts              timestamp,
    dev_id          bigint,
    power           float,
    speed           float,
    message         text
) USING MARS3 
  ORDER BY (dev_id,ts);

注意!
MARS3 表支持创建 Brin 索引,但非必须创建;MARS3 表建表时必须使用 ORDER BY 子句制定排序键。


1.2.2 配置项

注意!
此部分配置项为表级配置项,只能在创建数据表时使用 WITH(mars3options='a=1,b=2,...') 子句配置(MARS2 同样支持的参数 compress_threshold 除外,直接使用 WITH 子句配置即可),适用于单表,且一旦配置便无法修改。更多信息请见 数据表配置参数

以下参数用于调节 L0 层 Run 的大小,也可间接控制 L1 层之上的 Run 大小。

参数 单位 默认值 取值范围 描述
rowstore_size MB 64 8 ~ 1024 用于控制 L0 Run 何时切换。当数据大小超过该值,将会切换下一个 Run

以下参数用于设置压缩阈值,可用于调节压缩效果和改善读取效率,如果配置过低压缩效果不明显,配置过高消耗内存较多。

参数 单位 默认值 取值范围 描述
compress_threshold 元组 1200 1 ~ 100000 压缩阈值。用于控制单表每一列的多少元组(Tuple)进行一次压缩,是同一个单元中压缩的 Tuple 数上限

以下参数用于指定数据在 MARS3 中的加载模式。

参数 单位 默认值 取值范围 描述
prefer_load_mode normal normal / bluk 数据加载模式。normal 表示正常模式,新写入数据先写到 L0 层的行存 Run 中,积累到 rowstore_size 之后,落至 L1 层的列存 Run,相对于 bulk 模式会多一次 I/O,列存转换由同步变成了异步,但适用于 I/O 能力充足且对延迟敏感的高频次小批量写入场景;bluk 表示批量加载模式,适用于低频大批量写入场景,直接写至 L1 层的列存 Run,相对于 normal 模式,减少了一次 I/O,列存转换由异步变成了同步,适用于 I/O 能力不足且对延迟不敏感的低频大批量的数据写入

以下参数用于指定 Level 尺寸的放大系数。

参数 单位 默认值 取值范围 描述
level_size_amplifier 8 1 ~ 1000 Level 尺寸的放大系数。Level 触发合并操作的阈值,计算方式为:rowstore_size * (level_size_amplifier ^ level)。其值越大,读速越慢,写速越快。可以根据具体场景信息(写多读少/读多写少、压缩率等)来决定具体值。注意:确保每个 Level 的 run 数量不要过多,否则会影响查询性能,甚至阻止新数据插入

配置示例:

=# CREATE TABLE metrics (
    ts              timestamp,
    dev_id          bigint,
    power           float,
    speed           float
) USING MARS3
WITH (compress_threshold=1200,mars3options='rowstore_size=64',compresstype=zstd, compresslevel=1)
DISTRIBUTED BY (dev_id)
ORDER BY (dev_id,ts)
PARTITION BY RANGE (ts)
( START ('2023-07-01 00:00:00') INCLUSIVE
  END ('2023-08-01 00:00:00') EXCLUSIVE
  EVERY (INTERVAL '1 day')
,DEFAULT PARTITION OTHERS);

1.2.3 工具函数

  • matrixts_internal.mars3_level_stats:查看 MARS3 表每一个 Level 层级的状态,据此可以判断 MARS3 表的健康度,例如 Run 有没有按预期的进行合并,其个数是否符合预期等;
  • matrixts_internal.mars3_files:查看 MARS3 表文件状态,可以用来查看 MARS3 表的扩展文件和增量文件(Data 文件、Delta 文件、Index 文件等)是不是符合预期;
  • matrixts_internal.mars3_info_brin:查看 MARS3 表某个 Brin 索引的状态。


2 MARS2 概述

MARS2 存储引擎主要是面向数据高速加载和查询而设计的,其内部采用有序存储省却搜索。

2.1 内部原理

MARS2 表与 MARS3 相同,也采用 LSM Tree 结构存储。

MARS2 内部原理图如下:

我们将以概念逐层剖析的形式来解读上图。

2.1.1 排序键

  • MARS2 中,数据是有序存储的,在创建表时,需要通过创建索引的方式制定排序的顺序。 这个排序顺序涉及的字段,称为排序键。
  • 排序键只能指定一次,不能修改,不能删除。
  • 为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。
  • 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用 COLLATE C 能够加速排序。

2.1.2 Run 和元信息

  • 根据排序键,MARS2 中存储的数据是有序的,一段连续有序的数据称为 Run
  • 为了能够找到 Run 存储在哪里,会记录 Run 的存储位置,称为 Run 的元信息。
  • 同时,元信息还记录 Run 的最小最大值,这样可以在查询时进行过滤。
  • 插入数据时,会先在内存中对数据进行排序,因此 Run 的大小受制于排序内存大小。

2.1.3 合并

  • Run 中如果数据范围有重叠,则会造成读放大,降低查询效率。因此,当磁盘上的 Run 数量超过一定值时,MARS2 会将磁盘上的多个 Run 加载到内存,进行排序,最后输出为一个 Run。这个过程称为合并
  • 合并过程中,数据依然可读可写:
    • 读数据时,只会读合并的输入文件
    • 写数据时,合并过程不会读新写入的数据
    • 读,写,合并三者之间不会互相阻塞

2.1.4 Level

  • 为了让合并的输入文件尺寸相近(避免超大文件与小文件合并),Run 被组织到 Level 中,有 3 层:L0,L1,L2
  • 当数据合并完超过一定大小就会升级到更高的 Level 去,过程如下:
    • 1.新插入的数据在 L0,当 Run 数量达到一定数量时(可配置,见配置参数 mars2_automerge_threshold),触发 L0 合并,将 L0 所有的 Runs 合并为一个 Run。
    • 2.如果结果 Run 大小超过 25MB(可配置,见配置参数 level0_upgrade_size),则将其升级到 L1。
    • 3.如果升级到 L1 后,L1 的 Run 大小之和超过 1000MB(可配置,见 level1_upgrade_size),则触发合并,L1 的所有 Runs 合并为一个 Run。
    • 4.如果结果 Run 大小超过 1000MB(可配置,见 level1_upgrade_size),则将其升级到 L2。
    • 5.到达 L2 之后的 Run 不再合并。

2.1.5 列存

  • MARS2 采用列存方式存储数据,访问磁盘时只需要访问用到的列,这样减少了 I/O。
  • 同一列数据类型相同,更容易进行压缩,节省磁盘空间。
  • 另外,列式存储适合于向量化执行,可以大幅加速查询执行速度,具体见向量化执行引擎

2.1.6 MINMAX 过滤

  • 前面提到了 Run 的元信息存储了最小最大值,用于在查询时进行过滤。
  • 因为是通过元信息进行过滤,不需要加载数据本身,相比于顺序扫描(先访问数据本身,再进行过滤)其 I/O 开销更低,能更快的进行过滤。
  • 使用 MARS2 建表时不会默认记录最小最大值,需要显式声明

首先创建 matrixts 扩展:

=# CREATE EXTENSION matrixts ;

显式声明记录最小最大值。

=# CREATE TABLE metrics (
    ts              timestamp   ENCODING(minmax),
    dev_id          bigint      ENCODING(minmax),
    power           float,
    speed           float,
    message         text
) USING MARS2;

创建 mars2_btree 索引。

=# CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);
  • 排序键和非排序键都可以定义 MINMAX,定义在排序键上时效果更为显著,表达式中 MINMAX 全部大写或全部小写均可。
  • 另外,最多只能给 32 个列定义 MINMAX。

2.1.7 MARS2 Btree 索引

  • 除了 MINMAX,MARS2 还内置了一个索引(即建表时创建的索引)。
  • 目前只能定义一个索引,即只能有一个全局排序
  • 与普通 Btree 索引不同,MARS2 Btree 索引:
    • 是稀疏的,因此尺寸更小
    • 由于数据本身是有序的,因此不存在索引扫描产生的随机 I/O

2.1.8 压缩

同 MARS3。

  • 默认情况下,所有的数据列,采用 lz4 进行压缩。
  • 支持手动指定编码链压缩算法,可以整个表指定,也可以单个列指定。

2.1.9 数据写入

  • 数据通过 INSERT 写入到内存中,之后经历这些过程,生成一个 Run。
  • 如果插入数据量较大,超过了排序内存,则会生成多个 Run。
  • 排序内存的大小是可以配置的,详见 "2.2.2 配置项"。

2.1.10 更新和删除

  • MARS2 暂时不支持更新和删除。
  • 更新操作可以用 Unique Mode 代替更新操作。
  • 删除操作只能通过删除或者 TRUNCATE 分区表完成。

2.2 MARS2 使用

2.2.1 创建 MARS2 表

已创建 matrixts 扩展的前提下,最简洁的建表方式,只需要在 CREATE TABLE 语句加上 USING 子句,并创建索引即可。延伸示例请见表设计最佳实践

=# CREATE TABLE metrics (
    ts              timestamp,
    dev_id          bigint,
    power           float,
    speed           float,
    message         text
) USING MARS2;

=# CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);

2.2.2 配置项

注意!
下述内容中表级配置项,指的是只能在创建数据表时使用 WITH 子句配置,适用于单表,且一旦配置便无法修改的配置项。全局配置项指的是可在会话或系统级别配置,系统级别的修改需要执行 mxstop -u 才能生效的配置项。更多信息请见 数据表配置参数

以下几个参数用于控制合并(Merge),其影响合并的方式见上文 Level 部分。

合并控制参数 单位 默认值 取值范围 描述
mars2_automerge_threshold run 32 10 - 2048 用于控制 所有 的 MARS2 表,L0 达到多少个 Run 触发合并,为全局配置项。如果要为表单独指定可以使用表选项 level0_merge_threshold
level0_merge_threshold run 32 1 - 2048 用于控制单表 L0 达到多少个 Run 触发合并,为表级配置项
level0_upgrade_size MB 25 1 - 10000 控制单表 L0 -> L1 升级的大小,当 L0 发生合并后结果 Run 超过这个大小将升级到 L1,为表级配置项
level1_upgrade_size MB 1000 1 - 10000 控制单表 L1 -> L2 升级的大小,当 L1 发生合并后结果 Run 超过这个大小将升级到 L2,为表级配置项
  • 如果每次插入较少的数据(每次插入对应一个 Run),很快就会触发 Merge,这种 Merge 效率不高。此时可以调高 mars2_automerge_threshold / level0_merge_threshold 提高 Merge 输入的 Run 数量,降低 Merge 的频率。
  • 当 L0/L1 的 Merge 输出不到升级到 L1 的大小,下一轮的 Merge 会将其与新的 Run 进行合并,导致写放大。为了避免这种情况,可以降低 level0_upgrade_size / level1_upgrade_size 使得其升级到下一层。

以下参数用于压缩控制,调节压缩效果,如果配置过低压缩效果不明显,配置过高消耗内存较多。

压缩控制参数 单位 默认值 取值范围 描述
compress_threshold 元组 1200 1 - 100000 压缩阈值。用于控制单表多少元组(Tuple)进行一次压缩,是同一个单元中压缩的 Tuple 数上限,为表级配置项

以下参数用于内存控制。这部分参数控制插入数据时,使用的排序内存的大小,当有多张分区表被插入时,每个分区表会分配 mars2_sort_mem_core 配置的内存;如果不够用则扩大,但总量不会超过 mars2_sort_mem

排序内存参数 单位 默认值 取值范围 描述
mars2_sort_mem KB 2097152KB(2GB) 128KB - 2147483647KB (~ 2048GB) 控制每一个单个插入的排序内存大小,如果插入目标表是分区表,它们将共享这个大小,为全局配置项
mars2_sort_mem_core KB 16384KB(16MB) 128KB - 2147483647KB (~ 2048GB) 控制每一个单个分区表至少分配多少排序内存,为全局配置项

表配置项示例:

=# CREATE TABLE metrics (
    ts              timestamp,
    dev_id          bigint,
    power           float,
    speed           float,
    message         text
) 
USING MARS2
WITH (compress_threshold=1200,level0_merge_threshold=32);

=# CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);

全局配置项在会话级别修改值示例:

=# SET mars2_sort_mem TO 2097152;

全局配置项在系统级别修改值示例:

=# gpconfig -c mars2_sort_mem -v 2097152
=# \q
$ mxstop -u


3 HEAP 概述

HEAP 是 YMatrix 的默认存储引擎,又称作堆存储,从 PostgreSQL 继承而来,只支持行存储,不支持列存储及压缩。它基于 MVCC 机制实现,适用于有大量更新、删除需求的场景。

3.1 使用 MVCC 机制

在 MVCC 机制影响下,HEAP 表在处理更新和删除操作时,并没有真正删除数据,而只是依靠数据版本信息屏蔽了老的数据(控制了数据的可见性)。因此,HEAP 表大量进行更新或删除操作,占用的物理空间会不断增大,需要你有计划地定期使用 VACUUM 清理老数据。

3.2 HEAP 使用

你可以运用以下 SQL 语句在 YMatrix 中创建一个 HEAP 表。

=# CREATE TABLE disk_heap(
    time timestamp with time zone,
    tag_id int,
    read float,
    write float
)
DISTRIBUTED BY (tag_id);


4 AO 概述

以 AOCO、AORO 为存储引擎的表统称为 AO(Append-Optimized)表,又称作追加优化表,可以支持插入、更新与删除操作,支持压缩。
AORO 支持行存储,AOCO 支持列存储。

AO 表无论是在表的逻辑结构还是物理结构上,都与 HEAP 表存在很大的不同。如上节所述 HEAP 表使用 MVCC 机制控制更新及删除操作后数据的可见性,而 AO 表则使用一个附加的 bitmap 表来实现,这个表的的内容就是表示 AO 表中哪些数据是可见的。

对于有大量更新及删除操作的 AO 表,同样需要计划地定期清理老数据,不过在 AO 表中,清理数据工具 vacuum 需要对 bitmap 进行重置并压缩物理文件,因此通常比 HEAP 进度慢。

注意!
存储引擎详细信息、使用及最佳实践请见表设计最佳实践