为了提升JSONB数据的查询效率,Hologres从 V1.3版本开始支持对于JSONB类型开启列式存储优化,能够降低JSONB数据的存储大小并加速查询。本文将会为您介绍Hologres中列式JSONB的使用。
列式JSONB原理介绍
如下图所示开启JSONB列式存储优化后,系统会在底层自动将JSONB的列转换为强Schema的列式存储,查询JSONB中某一个Value时就可以直接命中指定列,从而提升查询性能。同时因为JSONB中的Value是按列式存储的,在存储层可以达到像普通结构化数据一样的存储和压缩效率,从而有效降低存储,实现降本增效。
JSONB列式存储优化功能对JSON类型数据不适用,实际使用过程中请不要对JSON类型开启列式存储优化。
使用限制
仅Hologres V1.3及以上版本支持JSONB类型开启列式存储,建议将Hologres实例版本升级至1.3.37及以上版本再开始使用列式JSONB功能,会获取更好的性能和更优的体验。升级请使用自助升级或加入实时数仓Hologres交流群申请升级实例,详情请参见如何获取更多的在线支持?。
JSONB的列存优化仅能用于列存表,行存表暂不支持,并且至少1000条数据才会触发列存优化。
当前仅支持如下操作符的列式存储优化,并且如果查询中使用不支持的操作符,反而可能会导致查询性能下降。
操作符
右操作数据类型
描述
操作与结果
->
text
通过键获得JSON对象域。
操作示例:
select '{"a": {"b":"foo"}}'::json->'a'
返回结果:
{"b":"foo"}
->>
text
以TEXT形式获得JSON对象域。
操作示例:
select '{"a":1,"b":2}'::json->>'b'
返回结果:
2
列式JSONB使用
开启列式JSONB
通过以下语句对某张表的某个JSONB列打开JSONB列存优化。
-- 打开xx表的xx列的JSONB列式存储优化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = ON);
table_name为表名称;column_name为列名称。
打开JSONB列存优化后,系统在Compaction时将历史数据都转为列存,待Compaction完毕即完成历史数据的列存化。
Compaction会消耗系统资源(比如内存),建议该操作在业务低峰期操作。可以使用
vacuum table_name;
命令强制触发Compaction操作,待vacuum命令执行完毕,Compaction操作就执行完毕了。Compaction完成后新写入的数据会按照列存存储。
开启Decimal类型推导
开启Decimal类型推导前,请确保已开启JSONB列存优化。
Hologres从 V2.0.11版本开始,支持将DECIMAL类型的数据进行列存优化。例如如下的JSON数据:
{
"name":"Mike",
"statistical_period":"2023-01-01 00:00:00+08",
"balance":123.45
}
balance
的数据在开启Decimal推导后,也支持按照列存优化。开启方法如下:
-- 打开xx表的xx列的Decimal列存优化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_decimal = ON);
table_name为表名称;column_name为列名称。
查看某张表的列式JSONB开启情况
通过以下语句查看某张表的列式JSONB开启情况。
Hologres V1.3.37及以上版本支持如下命令。
说明该命令在Hologres V2.0.17及以下版本只支持看
public
Schema下的表,从2.0.18版本开始支持查看其他Schema下的表开启情况。--2.0.17及以下版本仅支持查看public schema的表,2.0.18版本可以查询其他schema的表 SELECT * FROM hologres.hg_column_options WHERE schema_name='<schema_name>' AND table_name = '<table_name>';
其中schema_name为Schema名称,table_name为表名称。
Hologres V1.3.10~V1.1.36版本使用如下命令。
SELECT DISTINCT a.attnum as num, a.attname as name, format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as notnull, com.description as comment, coalesce(i.indisprimary,false) as primary_key, def.adsrc as default, a.attoptions FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid LEFT JOIN pg_index i ON (pgc.oid = i.indrelid AND i.indkey[0] = a.attnum) LEFT JOIN pg_description com on (pgc.oid = com.objoid AND a.attnum = com.objsubid) LEFT JOIN pg_attrdef def ON (a.attrelid = def.adrelid AND a.attnum = def.adnum) WHERE a.attnum > 0 AND pgc.oid = a.attrelid AND pg_table_is_visible(pgc.oid) AND NOT a.attisdropped AND pgc.relname = '<table_name>' ORDER BY a.attnum;
其中table_name为表名称。
示例返回结果。
返回结果可以看到某个列的attoptions或option属性为
enable_columnar_type = ON
,则表示已经配置成功。
关闭列式JSONB
通过以下命令关闭某张表的某个列的JSONB列存优化。
-- 关闭xx表的xx列的JSONB列式存储优化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = OFF);
table_name为表名称;column_name为列名称。
关闭JSONB列存优化后,系统在Compaction时将历史数据都转为标准的JSONB存储方式,待Compaction完毕即完成历史数据的转换。
Compaction会消耗系统资源(比如内存),建议该操作在业务低峰期操作。可以使用
vacuum table_name;
命令强制触发Compaction操作,待vacuum命令执行完毕,Compaction操作就执行完毕了。Compaction完成后,新写入的数据会按照JSONB格式存储。
关闭Decimal类型推导
通过以下命令关闭某张表的某个列的Decimal类型列存优化推导。
-- 关闭xx表的xx列的Decimal列存优化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_decimal = OFF);
table_name为表名称;column_name为列名称。
Decimal类型推导关闭后,会立刻触发Compaction,将原来已经列存优化后的Decimal类型的数据转换为原有模式。
设置Bitmap索引
在Hologres中,bitmap_columns
属性指定位图索引,是数据存储之外的独立索引结构,以位图向量结构加速等值比较场景,能够对文件块内的数据进行快速的等值过滤,适用于等值过滤查询的场景。Hologres从V2.0版本开始支持对开启了列存的JSONB设置Bitmap索引。开启列存JSONB后,系统会解析出int、int[]、bigint、bigint[]、text、text[]、jsonb这7种数据类型。开启Bitmap索引后,系统会对推导成int、int[]、bigint、bigint[]、text、text[]类型的数据建立Bitmap索引。
使用语法如下:
call set_table_property('<table_name>', 'bitmap_columns', '[<columnName>{:[on|off]}[,...]]');
参数说明:
参数 | 说明 |
table_name | 表名称。 |
columnName | 列名称。 |
on | 当前字段打开位图索引。 重要 仅支持开启了列存的JSONB设置Bitmap索引。 |
off | 当前字段关闭位图索引。 |
使用示例
创建表。
DROP TABLE IF EXISTS user_tags; -- 创建数据表 BEGIN; CREATE TABLE IF NOT EXISTS user_tags ( ds timestamptz, tags jsonb ); COMMIT;
打开
tags
列的JSONB列存优化。ALTER TABLE user_tags ALTER COLUMN tags SET (enable_columnar_type = ON);
查看JSONB列式存储开启情况。
select * from hologres.hg_column_options where table_name = 'user_tags';
如下返回结果中可以看到tags行的options属性是enable_columnar_type = on,表示已经配置成功。
schema_name | table_name | column_id | column_name | column_type | notnull | comment | default | options -------------+------------+-----------+-------------+--------------------------+---------+---------+---------+--------------------------- public | user_tags | 1 | ds | timestamp with time zone | f | | | public | user_tags | 2 | tags | jsonb | f | | | {enable_columnar_type=on} (2 rows)
导入数据。
INSERT INTO user_tags (ds, tags) SELECT '2022-01-01 00:00:00+08' , ('{"id":' || i || ',"first_name" :"Sig", "gender" :"Male"}')::jsonb FROM generate_series(1, 10001) i;
(可选)强制触发数据落盘。
写入数据后,系统会在数据落盘时进行JSONB的列存优化,为了尽快看到效果,此处使用如下后台命令,强制触发数据落盘。
VACUUM user_tags;
样例查询。
使用如下SQL查询
id
为10
的first_name
。SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
通过执行计划检查列存优化是否使用。
-- 显示详细的统计信息 SET hg_experimental_show_execution_statistics_in_explain = ON; -- 查看执行计划 EXPLAIN ANALYZE SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
结果中有
columnar_access_used
,表示JSONB使用了列存优化。针对步骤6中的查询,还可以对
tags
设置Bitmap索引,以提升针对某个key等值查询的效率,设置方法如下。call set_table_property('user_tags', 'bitmap_columns', 'tags');
通过执行计划检查Bitmap索引是否生效。
-- 查看执行计划 EXPLAIN ANALYZE SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
返回结果如下:
结果中有
bitmap_used
,表示使用了Bitmap索引。
列式JSONB不推荐的使用场景
使用列式JSONB不仅会降低存储,还会显著提升查询效率。但是列式JSONB并不是所有场景都适用,以下场景不建议使用,否则会事倍功半。
查询会带出完整JSONB列
Hologres的列式JSONB方案对于大部分使用场景都有比较好的优化效果,需要注意的是:对于查询结果需要带出完整JSONB列的场景,性能相较于直接存储原始格式的JSONB会有降低,比如以下SQL:
--建表DDL
CREATE TABLE TBL(key int, json_data jsonb);
SELECT json_data FROM TBL WHERE key = 123;
SELECT * FROM TBL limit 10;
原因在于底层已经将JSONB数据转成了列式存储,所以当需要查询出完整JSON数据的时候,就需要将那些已经列式存储的数据再重新拼装成原来的JSONB格式:
这个步骤就会产生大量的IO以及转换开销,如果涉及到的数据量很大,列数又很多,甚至可能成为性能瓶颈,所以此场景下建议不要开启列式优化。
极稀疏的JSONB数据
当Hologres列式化JSONB数据遇到稀疏的字段时,Hologres会将这部分字段合并至一个叫做holo.remaining
的特殊列中,以此来避免列数膨胀的问题。所以如果JSONB数据包含的都是稀疏字段,比如极端情况下每个字段都只会出现一次,那么列式化将不会起效,因为所有字段都是稀疏的,那么所有字段都会合并至holo.remaining
字段,等于没有进行列式化,这种情况下不会有查询性能的提升。
包含复杂嵌套结构的JSONB数据
如下JSONB数据的根节点就是一个数组,且该数组中存放的是非同构的JSONB数据,当前Hologres在列式化JSONB数据的时候,遇到类似复杂的嵌套结构,会将这部分数据退化成一列,所以此JSONB数据开启列式JSONB优化,将不会带来明显的查询性能收益。
'[
{"key1": "value1"},
{"key2": 123},
{"key3": 123.01}
]'
列式JSONB最佳实践
慢查询诊断
如果开启列式JSONB后,发现查询性能反而比不开启性能还要差很多,首先排查查询是否带出了完整JSONB列,如果SQL过于复杂,可以使用Explain Analyze
方式来诊断,SQL命令示例如下:
CREATE TABLE TBL(key int, json_data json); --建表DDL
ALTER TABLE TBL ALTER COLUMN json_data SET (enable_columnar_type = on);
Explain Analyze SELECT json_data FROM TBL WHERE key = 123;
Explain Analyze的结果中会有Hint的信息,如果在Hint信息中有以下内容则代表查询带出了完整的JSONB列,导致了性能的退化:
Column 'json_data' has enabled columnar jsonb, but the query scanned the entire Jsonb value
更优的SQL写法
将JSONB字段数据转成TEXT格式有不同的写法,但是使用
->>
操作符的性能会更好,比如要获取json_data列中的name属性:--性能更好 SELECT json_data->>'name' FROM tbl; --性能一般 SELECT (json_data->'name')::text FROM tbl;
如果JSON的某个字段中存储的是TEXT数组,需要判断数组中是否包含特定值,建议使用以下写法:
SELECT key FROM tbl WHERE jsonb_to_textarray(json_data->'phones') && ARRAY['123456'];
常见问题
开启列存化后为什么存储上涨?
开启列式JSONB优化后,原JSONB数据中的字段名都不会再存储了,而只需存储每个字段对应的具体值,且列式化后每列的数据类型都是一样的,列式存储能有比较好的数据压缩率,理论上数据的存储空间会有明显的下降。
但如果JSONB数据中的字段比较稀疏,列数膨胀比较厉害,那么列式化后的每一列都会带来额外的存储开销(列的统计信息、索引等),且如果列式化后每一列的类型都是TEXT类型,压缩效果就不会很好。所以实际的存储压缩效率与实际业务的数据有关(比如稀疏度等),不一定所有的数据都有很好的压缩效果。