赞
踩
列存技术能够显著提升分析类查询的查询效率,这也是OceanBase实现HTAP和OLAP的关键特性之一。本文旨在探讨OceanBase列存的独特实现方式。
自诞生之初,OceanBase便坚定地采用LSM-Tree架构,经过持续的性能提升,已能够完美支持各类典型的TP类型业务,并能够在各种极限负载压力下保持其性能。凭借大量的工程实践经验,OceanBase成功打造出一套拥有鲜明特色的、业界领先的LSM-tree存储引擎。而常见的OLAP场景主要涉及批量写入,而随机更新则相对较少,这使得数据在列存组织下能尽量保持相对静态,这种特性使得LSM-Tree架构在OLAP场景中尤为适用。
在4.3版本,基于原有技术积累,OceanBase 存储引擎继续扩展,实现对列存的支持,实现存储一体化,一套代码一个架构一个OBServer,列存数据和行存数据完美共存,这样真正实现了对TP类和AP类查询的性能的兼顾。
OceanBase作为原生分布式数据库,用户数据默认会多副本存储,为了利用多副本的优势,为用户提供数据强校验以及迁移数据重用等进一步的增强体验,自研的LSM-Tree存储引擎也做了较多的针对性设计,首先每个用户数据整体可以分成两个大部分基线数据和增量数据。
基于列存应用场景随机更新量可控的背景,OceanBase结合自身基线数据和增量数据的特质,提出了一套对上层透明的列存实现方式:
我们不仅在存储引擎中实现了列存模式,为了让用户能够更容易从其它 OLAP 数据库迁移过来,以及帮助之前有 OLAP 需求的 OceanBase 老客户升级到列存,从优化器到执行器以及存储其它相关模块,都针对列存进行了适配以及优化,让用户迁移到列存后基本对业务无感,能够像使用行存一样享受到列存带来的性能优势。 也让OceanBase真正实现了TP/AP一体化,实现一套引擎一套代码支持不同类型业务的目标,打造完善的HTAP引擎。
引入新的列存存储模式之后,数据合并行为和原有行存数据有较大变化,由于增量数据全部是行存,需要和基线数据合并后拆分到每个列的独立SSTable中,合并时间和资源占用相对行存会有较大增长.
为了加速列存表合并速度,Compaction流程进行大幅增强,对于列存表,除了能够像行存表一样进行水平拆分并行合并加速之外,还增加了垂直拆分加速,列存表会降多个列的合并动作放在一个合并任务内进行,并且一个任务内的列数能够根据系统资源自主选择升降,保证整体在合并速度以及内存开销达到更好的平衡。
OceanBase一直以来存储数据会经过两级压缩,第一级是OceanBase自研的行列混合编码压缩,第二级是通用压缩,其中行列混合编码由于是数据库内置算法,因此可以支持不解压直接查询,同时可以利用编码信息进行查询过滤加速,尤其对AP类查询会有极大的加速。
但是原有行列混合编码算法仍然偏向行组织,因此针对列存表实现了全新的列式编码算法,相比原有编码算法,新算法支持查询的全面向量化执行,支持兼容不同指令集的SIMD优化,同时针对数值类型大幅提高压缩比,实现对原有算法在性能和压缩比上的全面提升。
常见列存数据库一般均会对每列数据按照一定的粒度进行预聚合计算,聚合的结果随数据一起持久化,当用户查询请求访问列数据时,数据库能够通过预聚合数据过滤数据,大幅减少数据访问开销,减少不必要的IO消耗。
在列存引擎中,我们同样增加了skip index的支持,针对每列数据会按照微块粒度进行最大值、最小值、和以及null总量等多个维度的聚合计算,并逐层向上聚合累加获得宏块、SSTable等更大粒度的聚合值,用户查询能够根据扫描范围不断下钻选取合适粒度聚合值进行过滤以及聚合输出。
OceanBase 在3.2版本开始初步支持简单的查询下压,从4.x版本开始存储全面支持了向量化以及更多的下压支持,在列存引擎中,下压功能进一步得到增强和扩展,具体包括:
对于 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 = 0
- WITH 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)

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。