暂无图片
暂无图片
2
暂无图片
暂无图片
1
暂无图片

深入剖析OceanBase 4.3的列存技术

原创 OceanBase数据库 2024-05-27
517

在大规模数据复杂分析或海量数据即席查询的场景中,列式存储是一项关键能力。与行式存储相比,列式存储通过不同的数据文件组织方式,将表中的数据按列进行物理排列。这种存储方式使得在分析场景中,查询计算只需扫描所需的列数据,避免了整行扫描,从而减少了 IO 和内存等资源的使用,提升了计算速度。此外,列式存储天然具备更好的数据压缩条件,能够实现较高的压缩比,减少了存储空间和网络传输带宽的需求。


常见的列存存储引擎在实现上通常假设不会有大量随机更新,并尽量保持列存数据的静态性。然而,当面对大量数据随机更新时,系统性能问题是不可避免的。OceanBase 的 LSM-Tree 架构通过分别处理基线数据和增量数据,解决了这一问题。因此,OceanBase 4.3 版本在现有架构基础上进行了扩展,推出了列存引擎,实现了列存和行存数据存储的一体化,兼顾事务处理(TP)和分析处理(AP)查询的性能。


为了让有分析需求的用户顺畅使用新版本,围绕列存引擎,从优化器到执行器,从 DDL 到事务处理等多个模块都进行了适配和优化。包括基于列存的新的代价模型和向量化引擎,查询下压功能的扩展和增强,跳跃索引(Skip Index),新的列式编码算法,自适应压缩(Compaction)等。本文将深入剖析 OceanBase 4.3 版本带来的列存能力、应用场景以及用户关注的未来发展规划。

一、列存整体架构

OceanBase 作为原生分布式数据库,默认情况下会为用户数据创建多个副本。为了充分利用多副本的优势,为用户提供数据强校验和数据迁移重用等增强体验,OceanBase 自研的 LSM-Tree 存储引擎做了深度优化:
  • 基线数据:相较于业内常见的 LSM-Tree 实现逻辑,OceanBase 提出了"每日合并"的概念。用户可定期或根据操作选择一个全局版本号,所有副本的租户数据将在这个版本上进行一轮 Major Compaction,生成这个版本的基线数据。所有副本在同一版本下的基线数据完全一致,物理上保持一致。

  • 增量数据:相对于基线数据,增量数据是指在最新版本的基线数据之后写入的数据。增量数据可以是刚写入Memtable的内存数据,也可以是已经转储为SSTable 的磁盘数据。增量数据在每个副本中独立维护,不保证一致性,并且包含了所有多版本的数据。

基于列存应用场景随机更新量可控的背景,OceanBase 4.3 结合自身基线数据和增量数据的特质,提出了一套对上层透明的列存实现方式:基线数据存储为列存模式,增量数据保持行存,确保用户所有 DML 操作不受影响,上下游同步无缝接入,列存表数据仍然可以像行存表一样进行所有事务操作。列存模式下每列数据存储为一个独立 SSTable,所有列的 SSTable 组合成为一个虚拟 SSTable 作为用户的列存基线数据。同时,用户可根据实际业务诉求在建表环节指定设置,基线数据可以支持行存、列存、行存列存冗余三种模式,提供更好的灵活性。

图片

OceanBase 4.3 版本中不仅在存储引擎中实现了列存模式,更从优化器、执行器以等多维度进行列存的适配优化。用户在迁移到列存模式后基本上不会感受到业务变化,能够像使用行存一样享受到列存带来的性能优势。列存引擎的全面优化,也使得 OceanBase 真正实现了 TP & AP 一体化,实现了一套引擎、一套代码支持不同类型业务的目标,打造更加完善的 HTAP 混合负载实时分析能力。

二、OceanBase实现列存有哪些天然优势

(一)成熟的 LSM-Tree 引擎

与传统数据库相比,OceanBase 拥有天然的 Delta Store,非常适合实现列存。基于 LSM-Tree 存储引擎的支持,OceanBase 列存不仅支持完整的事务,而且基础算子的性能不弱于传统的 TP 数据库。在列存上,完整的事务支持使得 OceanBase 在更新方面具有天然优势,所有事物语义和多样事物的管理对用户来说完全透明的,用户可以轻松切换到列存模式,将列存数据库当成行存数据库使用,对业务完全透明,不需要做任何改动。

(二)完善的执行引擎

OceanBase 不仅拥有完整的执行引擎,还具备通用的优化器是通用的。在行存模式下,OceanBase 已经实现向量化存储引擎的无缝对接,无需任何修改即可支持向量化执行。此外,OceanBase 实现一套优化器的代码在上层对行存和列存进行不同代价的估算,使得用户的 SQL 可以自动选择行存或列存。

(三)灵活的原生分布式

OceanBase 天然支持分布式并行查询引擎,未来还可以轻松扩展到列存异构副本。列存异构副本的优势体现在用户需要完全硬隔离的应用场景中,未来的OceanBase 版本将新增这一功能。

综上所述,OceanBase 凭借其天然优势推动了 4.3 版本中列存功能的实现。引入列存储引擎后,OceanBase 整体架构在外部表现上完全不变,并且从架构层面支持了列存相关的三种模式:
  • 基线列存 +增量行存:基线数据采用列存方式存储,增量数据采用行存方式存储。

  • 灵活的行存/列存索引:可以对行存表建立列存索引,也可以对列存表建立行存索引,还可以对两者进行任意组合。由于所有列存表和索引的底层存储结构是统一的,因此 OceanBase 可以自动支持列存和行存的索引。

  • 列存副本:OceanBase 正在研发的列存副本功能。得益于原生分布式能力,只需对模式或表做部分修改,即可以通过 Compaction 将新增的只读副本转换为列存存储模式。

三、列存使用方法

(一)默认创建列存表

对于 OLAP 业务需求,我们推荐默认创建列存表。如何确保租户创建出来的表默认为列存表?只通过下面的配置项即可实现:
alter system set default_table_store_format = "column";
复制
随后我们创建的表格没有指定 column group 时,默认创建为列存表。
OceanBase(root@test)>create table  t1 (c1 int primary key, c2 int ,c3 int);Query OK,0 rows affected (0.301 sec)
OceanBase(root@test)>show create table t1;
CREATE TABLE `t1` ( `c1` int(11) NOT NULL, `c2` int(11) DEFAULT NULL, `c3` int(11) DEFAULT NULL, PRIMARY KEY (`c1`)) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0WITH COLUMN GROUP(each column)
1 row in set (0.101 sec)
复制

(二)指定创建列存表

为了方便用户创建列存表,列存引入新的语法 with column group,当用户建表时最后指定 `with column group(each column)` ,即表示创建列存表。
OceanBase(root@test)>create table  tt_column_store (c1 int primary key, c2 int ,c3 int) with column group (each column);Query OK,0 rows affected (0.308 sec)
OceanBase(root@test)>show create table tt_column_store;
CREATE TABLE `tt_column_store` ( `c1` int(11) NOT NULL, `c2` int(11) DEFAULT NULL, `c3` int(11) DEFAULT NULL, PRIMARY KEY (`c1`)) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0 WITH COLUMN GROUP(each column)
1 row in set (0.108 sec)
复制

(三)指定创建列存行存冗余表

在某些场景下,用户可以容忍一定程度的数据冗余,以满足 AP/TP 业务场景的双重需求。此时,可以增加行存数据的冗余,通过 `with column group` 语法增加指定 `all columns` 即可实现。
create table  tt_column_row (c1 int primary key, c2 int , c3 int) with column group (all columns, each column);Query OK, 0 rows affected (0.252 sec)
OceanBase(root@test)>show create table tt_column_row;CREATE TABLE `tt_column_row` ( `c1` int(11) NOT NULL, `c2` int(11) DEFAULT NULL, `c3` int(11) DEFAULT NULL, PRIMARY KEY (`c1`)) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0 WITH COLUMN GROUP(all columns, each column)
1 row in set (0.075 sec)
复制

(四)列存扫描

如何查看是否列存扫描计划?计划展示上新增 COLUMN TABLE FULL SCAN,描述列存表的范围扫描。
OceanBase(root@test)>explain select * from tt_column_store;+--------------------------------------------------------------------------------------------------------+| Query Plan                                                                                             |+--------------------------------------------------------------------------------------------------------+| =================================================================                                      || |ID|OPERATOR              |NAME           |EST.ROWS|EST.TIME(us)|                                      || -----------------------------------------------------------------                                      || |0 |COLUMN TABLE FULL SCAN|tt_column_store|1       |7           |                                      || =================================================================                                      || Outputs & filters:                                                                                     || -------------------------------------                                                                  ||   0 - output([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), filter(nil), rowset=16 ||       access([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), partitions(p0)         ||       is_index_back=false, is_glOceanBaseal_index=false,                                                      ||       range_key([tt_column_store.c1]), range(MIN ; MAX)always true                                     |+--------------------------------------------------------------------------------------------------------+
复制
计划展示上新增 COLUMN TABLE GET,描述列存表上的指定主键的 get 操作。
OceanBase(root@test)>explain select * from tt_column_store where c1 = 1;+--------------------------------------------------------------------------------------------------------+| Query Plan                                                                                             |+--------------------------------------------------------------------------------------------------------+| ===========================================================                                            || |ID|OPERATOR        |NAME           |EST.ROWS|EST.TIME(us)|                                            || -----------------------------------------------------------                                            || |0 |COLUMN TABLE GET|tt_column_store|1       |14          |                                            || ===========================================================                                            || Outputs & filters:                                                                                     || -------------------------------------                                                                  ||   0 - output([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), filter(nil), rowset=16 ||       access([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), partitions(p0)         ||       is_index_back=false, is_global_index=false,                                                      ||       range_key([tt_column_store.c1]), range[1 ; 1],                                                   ||       range_cond([tt_column_store.c1 = 1])                                                             |+--------------------------------------------------------------------------------------------------------+12 rows in set (0.051 sec)
复制
如何通过 Hint 指定列存行存冗余表走列存扫描?对于列存行存冗余表,优化器会根据代价选择走行存或者列存扫描,如简单场景做全表扫描,会默认使用行存生成计划。
OceanBase(root@test)>explain select * from tt_column_row;+--------------------------------------------------------------------------------------------------+| Query Plan                                                                                       |+--------------------------------------------------------------------------------------------------+| ========================================================                                         || |ID|OPERATOR       |NAME         |EST.ROWS|EST.TIME(us)|                                         || --------------------------------------------------------                                         || |0 |TABLE FULL SCAN|tt_column_row|1       |3           |                                         || ========================================================                                         || Outputs & filters:                                                                               || -------------------------------------                                                            ||   0 - output([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), filter(nil), rowset=16 ||       access([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), partitions(p0)         ||       is_index_back=false, is_global_index=false,                                                ||       range_key([tt_column_row.c1]), range(MIN ; MAX)always true                                 |+--------------------------------------------------------------------------------------------------+
复制
如果用户希望通过手动调优走列存扫描,可以通过 hint USE_COLUMN_TABLE 来强制 tt_column_row 表走列存扫描。
OceanBase(root@test)>explain select /*+ USE_COLUMN_TABLE(tt_column_row) */ * from tt_column_row;+--------------------------------------------------------------------------------------------------+| Query Plan                                                                                       |+--------------------------------------------------------------------------------------------------+| ===============================================================                                  || |ID|OPERATOR              |NAME         |EST.ROWS|EST.TIME(us)|                                  || ---------------------------------------------------------------                                  || |0 |COLUMN TABLE FULL SCAN|tt_column_row|1       |7           |                                  || ===============================================================                                  || Outputs & filters:                                                                               || -------------------------------------                                                            ||   0 - output([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), filter(nil), rowset=16 ||       access([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), partitions(p0)         ||       is_index_back=false, is_global_index=false,                                                ||       range_key([tt_column_row.c1]), range(MIN ; MAX)always true                                 |+--------------------------------------------------------------------------------------------------+
复制
类似的方式,通过 Hint NO_USE_COLUMN_TABLE 可以强制表不进行列存扫描。
OceanBase(root@test)>explain select  /*+ NO_USE_COLUMN_TABLE(tt_column_row) */ c2 from tt_column_row;+------------------------------------------------------------------+| Query Plan                                                       |+------------------------------------------------------------------+| ========================================================         || |ID|OPERATOR       |NAME         |EST.ROWS|EST.TIME(us)|         || --------------------------------------------------------         || |0 |TABLE FULL SCAN|tt_column_row|1       |3           |         || ========================================================         || Outputs & filters:                                               || -------------------------------------                            ||   0 - output([tt_column_row.c2]), filter(nil), rowset=16         ||       access([tt_column_row.c2]), partitions(p0)                 ||       is_index_back=false, is_global_index=false,                ||       range_key([tt_column_row.c1]), range(MIN ; MAX)always true |+------------------------------------------------------------------+11 rows in set (0.053 sec)
复制

四、未来展望

OceanBase 4.3 列存的引入,为用户的数据分析以及实时分析场景提供了新的选择。未来,OceanBase 列存将持续演进,为用户带来更加丰富的 feature、更强劲的性能以及更灵活的部署模式。

第一,更丰富的功能。目前,我们支持纯列存储引擎,未来将实现可自定义的灵活列组组织支持,满足不同场景的分析需求。此外,我们计划将增量旁路导入功能进一步增强,帮助用户实现高效的数据导入,缩短数据分析准备时间。

第二,更好的性能。增强 Skip Index 的支持,使其能够更好地满足用户的查询需求。此外,我们计划实现格式一体化,目前存储的格式多样化,未来将实现存储格式与 SQL 向量化引擎的紧密结合,使得在执行 SQL 计算时,系统能够识别不同的存储格式,从而帮助用户节省更多的数据转换开销。

第三,更灵活的部署模式。在未来的版本中,我们将支持 OLAP 所需的异构副本,以满足用户对强依赖异构副本的需求。此外,未来还将支持存算分离模式,使得所有用户的 AP 数据库都能够以更低的成本享受存储与计算的分离。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论

胡飞
暂无图片
11月前
评论
暂无图片 0
深入剖析OceanBase 4.3的列存技术
11月前
暂无图片 点赞
评论
P
暂无图片
获得了6次点赞
暂无图片
内容获得1次评论
暂无图片
获得了18次收藏
TA的专栏
OceanBase 产品特性
收录26篇内容
OceanBase 用户实践
收录16篇内容
OceanBase 技术解读
收录10篇内容
目录
  • 一、列存整体架构
  • 二、OceanBase实现列存有哪些天然优势
    • (一)成熟的 LSM-Tree 引擎
    • (二)完善的执行引擎
    • (三)灵活的原生分布式
  • 三、列存使用方法
    • (一)默认创建列存表
    • (二)指定创建列存表
    • (三)指定创建列存行存冗余表
    • (四)列存扫描
  • 四、未来展望