写点什么

HStore 表全了解:实时入库与高效查询利器

  • 2023-06-14
    广东
  • 本文字数:5379 字

    阅读完需:约 18 分钟

HStore表全了解:实时入库与高效查询利器

本文分享自华为云社区《GaussDB(DWS)HStore表讲解》,作者:大威天龙:- 。

HStore 表简介


面对实时入库和实时查询要求越来越高的趋势,已有的列存储无法支持并发更新入库,行存查询性能无法做到实时返回且空间压缩表现不佳。GaussDB(DWS)基于列存储格式设计和实现了全新的 HStore 表,同时提供高效的并发插入、更新入库,以及高性能实时查询。本文章将从使用者角度介绍 HStore 概念以及使用。

HStore 表的背景


为什么要有 HStore 表呢?在具体讲解 HStore 表之前,我们先来回顾一下 GaussDB(DWS)中几种已有的表类型:

行存表(row-store)


最基础的表类型,顾名思义,数据按行存储,在实际的物理块中,数据的将按下列图示的方式存储:



优势很明显,点查场景下,直接就能索引到行存某行元组的位置,点查性能好。数据库中的系统表就是行存表,对于用户的一些对点查性能要求高或者频繁更新的小表,都推荐用行存表。

列存表(column-store)


AP 场景下,常常需要对某列进行批量查询来做分析业务,这时候采用行存的话就会把所有列都读出来产生冗余 IO, 同时 AP 场景下的表数据量往往很大,行存表压缩暂未商用,使用行存表也会导致占用空间过大。


GaussDB(DWS)中的列存表就是针对这种场景实现的,列存表数据的实际存储示意图如下:



列存表将每列的数据批量存储成一个 CU(Compress Unit), 能带来了很好的空间压缩与批量查询性能提升,对于一些涉及多表关联的分析类复杂查询、数据不经常更新的表,推荐使用列存表。

列存带 Delta 表


对于列存表,如果业务是频繁的小批量插入,那么将产生大量的小 CU(单个 CU 里只有几百条甚至几条数据), 每个列的 CU 都是有压缩代价的,小 CU 过多将严重影响列存表的查询性能。


列存的 Delta 表就是针对这种场景实现的,让小批量插入的数据先存储到行存 delta 表,满 6w 后由后台 autovacuum 异步 merge 到主表 CU。



需要注意的是列存带 Delta 表只解决小批量入库产生的小 CU 问题,不解决同一个 CU 上的并发更新问题

HStore 表


前面提到,虽然列存老 Delta 表解决了小批量入库产生的小 CU 问题,但是没有解决同一个 CU 上的并发更新产生的锁冲突问题。


而实时入库的场景下,需要将 insert+upsert+update 操作实时并发入库,数据来源于上游的其他数据库或者应用,同时要求入库后的数据要能及时查询,且对于查询的效率要求很高。


目前的列存表由于锁冲突的原因无法支持并发 upsert/update 入库,导致这些有需要的局点只能使用行存表,但是行存表因为格式的天然劣势,在 AP 查询场景下一方面性能较慢,另一方面由于压缩差导致占用了大量的磁盘空间,对用户产生额外成本。


GaussDB(DWS)中的 HStore 表, 在使用列存储格式尽量降低磁盘占用的同时,支持高并发的更新操作入库以及高性能的查询效率。面向对于实时入库和实时查询有较强诉求的场景,同时拥有处理传统 TP 场景的事务能力。


HStore 表的示意图如下:


GaussDB(DWS) 中几种表类型的对比


HStore 的 Delta 表


HStore 表的实现主要依靠一张新设计的 delta 表以及内存并发控制机制,这里简单讲一下 delta 表的实现以及简单的观察 delta 表。


HStore 的 Delta 表主要用于存放入库产生的 Insert/Delete/Update 操作,小批量 Insert 的数据会先进入 Delta 形成一条类型是 I(Insert)的记录;删除会往 Delta 表插入一条类型是 D(Delete)的记录;更新操作(Upsert 与 Update)会拆分成 Delete + Insert,会插入一条类型 X(表示由更新产生的删除)的记录以及一条类型 I 的记录;


(类型是 U(Update)的记录由轻量化 Update 产生,不过当前轻量化更新默认关闭,所以不用管。)

可以看到,入库时的 Upsert/Update/Delete 都会转换成相应类型的记录插入的 HStore 的 Delta 表中,再结合内存并发控制机制,就能保证同一个 CU 上更新于删除操作不会阻塞。同时,由于小批量的插入只会在 Delta 表上形成一条记录,相比与列存老 Delta 的直接存储数据,能减少 IO 占用,提高 MERGE 效率。

HStore 的 Delta 表 与 列存老 Delta 表的对比


HStore 的视图与函数


当前 HStore 表提供了视图,可以用来观察 Delta 表的给类型元组数量以及 Delta 的膨胀情况。


select * from pgxc_get_hstore_delta_info('tableName');
复制代码


同时也提供了函数可以对 Delta 表做轻量清理以及全量清理。


-- 轻量Merge满6万的I记录以及CU上的删除信息,持有四级锁不阻塞业务增删改查,但空间不会还给操作系统。select hstore_light_merge('tableName'); -- 全量Merge所有记录,然后truncate清空Delta表返还空间给系统,不过持有八级锁会阻塞业务。select hstore_full_merge('tableName');
复制代码


这里做一个简单的观察实验:


1.往 HStore 表上批量插入一百条数据,能看到生成了一条类型是 I 的记录(n_i_tup 为 1)


gaussdb=# create table data(a int primary key, b int);NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "data_pkey" for table "data"CREATE TABLEgaussdb=# insert into data values(generate_series(1,100),1);INSERT 0 100gaussdb=# create table hs(a int primary key, b int)with(orientation=column, enable_hstore=on);NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "hs_pkey" for table "hs"CREATE TABLEgaussdb=# insert into hs select * from data;INSERT 0 100gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --观察hstore表的delta表上的各类型数据 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size-----------+---------------------+----------+----------+----------+----------+----------+----------+----------- dn_1      | non partition table | 1 | 1 | 0 | 0 | 0 | 0 | 8192(1 row)
复制代码


2.执行 hstore_full_merge 后能观察到 Delta 表上没有元组(live_tup 为 0),并且 Delta 表的空间大小 data_size 是 0.


gaussdb=# select hstore_full_merge('hs'); hstore_full_merge------------------- 1(1 row)gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --观察hstore表的delta表上的各类型数据 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size-----------+---------------------+----------+----------+----------+----------+----------+----------+----------- dn_1      | non partition table | 0 | 0 | 0 | 0 | 0 | 0 | 0(1 row)
复制代码


3.执行删除,能观察到 Delta 表上有一条类型是 D 的记录(n_d_tup 为 1)。


gaussdb=# delete hs where a = 1;DELETE 1gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --观察hstore表的delta表上的各类型数据 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size-----------+---------------------+----------+----------+----------+----------+----------+----------+----------- dn_1      | non partition table | 1 | 0 | 1 | 0 | 0 | 0 | 8192(1 row)
复制代码


其它的操作这里不再一一尝试,感兴趣的读者可以自己下来试一下。

HStore 表的简单使用实验

准备工作


当需要使用 HStore 表时,需要同步修改以下几个清理相关的参数默认值,否则会导致 HStore 表性能严重劣化。推荐的参数修改配置是:autovacuum_max_workers_hstore=3,autovacuum_max_workers=6,autovacuum=true。

并发更新实验


在列存表上插入一批数据后,开启两个会话


1.会话 1 删除某一条数据,然后不结束事务:


gaussdb=#  create table col(a int , b int)with(orientation=column);CREATE TABLEgaussdb=# insert into col select * from data;INSERT 0 100gaussdb=# begin;BEGINgaussdb=# delete col where a = 1;DELETE 1
复制代码


2.会话 2 删除另一条数据,能看到会话 2 等待会话 1


gaussdb=# begin;BEGINgaussdb=# delete col where a = 2;
复制代码


会话 1 提交后会话 2 才能继续执行,这就复现了列存的 CU 锁问题:


3. 使用 HStore 表重复上面实验,能观察到会话 2 直接执行成功,不会锁等待。


gaussdb=# begin;BEGINgaussdb=# delete hs where a = 2;DELETE 1
复制代码

压缩效率实验


1.构建一张有三百万数据的数据表 data


gaussdb=# create table data( a int, b bigint, c varchar(10), d varchar(10));CREATE TABLEgaussdb=# insert into data values(generate_series(1,100),1,'asdfasdf','gergqer');INSERT 0 100gaussdb=# insert into data select * from data;INSERT 0 100gaussdb=# insert into data select * from data;INSERT 0 200---循环插入,直到数据量达到三百万gaussdb=# insert into data select * from data;INSERT 0 1638400gaussdb=# select count(*) from data;  count--------- 3276800(1 row)
复制代码


2.批量导入到行存表,观察大小为 223MB


gaussdb=# create table row (like data including all);CREATE TABLEgaussdb=# insert into row select * from data;INSERT 0 3276800gaussdb=#  select pg_size_pretty(pg_relation_size('row')); pg_size_pretty---------------- 223 MB(1 row)
复制代码


3.批量导入到列存表,观察大小为 3.5MB


gaussdb=# create table hs(a int, b bigint, c varchar(10),d varchar(10))with(orientation= column, enable_hstore=on);CREATE TABLEgaussdb=# insert into hs select * from data;INSERT 0 3276800gaussdb=#  select pg_size_pretty(pg_relation_size('hs')); pg_size_pretty---------------- 3568 KB(1 row)
复制代码


4.总结


这个表结构比较简单,数据也都是重复数据,所以 HStore 表的压缩效果很好,一般情况下 HStore 表相比行存能有 3-5 倍的压缩。

批量查询性能实验


还是使用上面建的表,这里简单验证一下批量查询


1.查询行存表的第四列,耗时在 4s 左右


gaussdb=# explain analyze select d from data;explain analye                                                               QUERY PLAN-----------------------------------------------------------------------------------------------------------------------------------------  id |          operation           |        A-time | A-rows | E-rows | Peak Memory  | E-memory | A-width | E-width | E-costs ----+------------------------------+----------------------+---------+---------+--------------+----------+---------+---------+---------- 1 | ->  Streaming (type: GATHER) | 4337.881 | 3276800 | 3276800 | 32KB         | | | 8 | 61891.00 2 | ->  Seq Scan on data | [1571.995, 1571.995] | 3276800 | 3276800 | [32KB, 32KB] | 1MB      | | 8 | 61266.00
复制代码


2.查询 HStore 表的第四列,耗时 300 毫秒左右


gaussdb=# explain analyze select d from hs;                                                                    QUERY PLAN---------------------------------------------------------------------------------------------------------------------------------------------------  id |               operation                |       A-time | A-rows | E-rows |  Peak Memory   | E-memory | A-width | E-width | E-costs ----+----------------------------------------+--------------------+---------+---------+----------------+----------+---------+---------+---------- 1 | -> Row Adapter                        | 335.280 | 3276800 | 3276800 | 24KB           | | | 8 | 15561.80 2 | ->  Vector Streaming (type: GATHER) | 111.492 | 3276800 | 3276800 | 96KB           | | | 8 | 15561.80 3 | -> CStore Scan on hs | [111.116, 111.116] | 3276800 | 3276800 | [254KB, 254KB] | 1MB      | | 8 | 14936.80
复制代码


3.总结


这里只验证了批量查询场景,该场景下列存以及 HStore 表相比行存都有很好的查询性能。但在索引点查询场景下,列存是比不上行存的,这里不再做详细对比。

HStore 表注意事项


1.参数设置


HStore 依赖后台常驻线程对 HStore 表进行 MERGE 清理操作,才能保证查询性能与压缩效率,所以使用 HStore 表务必设置相关 GUC,推荐的配置如下:


autovacuum_max_workers_hstore=3autovacuum_max_workers=6autovacuum=true
复制代码


2.并发同一行:


当前 HStore 并发更新同一行仍然是不支持的,其中同一行上并发 update/delete 操作会先等锁然后报错,同一行上的并发 upsert 操作会先等锁然后继续执行。由于等待开销也是会影响业务的入库性能,甚至可能产生死锁,所以需要在入库时保证不会并发更新到同一行或者同一个 key。


3.索引相关


索引会占用额外的空间,同时带来的点查性能提升有限,所以 HStore 表只建议在需要做 Upsert 或者有点查(这里指唯一性与接近唯一的点查)的诉求下创建一个主键或者 btree 索引。


4.MERGE 相关


由于 HStore 表依赖后台 autovacuum 来将操作 MERGE 到主表,所以入库速度不能超过 MERGE 速度,否则会导致 delta 表的膨胀,可以通过控制入库的并发来控制入库速度。同时由于 Delta 表本身的空间复用受 oldestXmin 的影响,如果有老事务存在可能会导致 Delta 空间复用不及时而产生膨胀。


5.UPSERT 性能


HStore 表虽然相比普通列存,并发 upsert 入库性能得到了很大提升,但相比行存还是有差距,大概只有行存的 1/3。所以在不追求压缩率以及批量查询性能、只追求单点查询性能的场景下,还是推荐行存表入库。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
HStore表全了解:实时入库与高效查询利器_数据库_华为云开发者联盟_InfoQ写作社区