加速和扩大洞察|如何做好半结构化数据分析

背景

数据仓库擅长对结构化数据进行管理、分析。针对半结构化数据,不少数据仓库产品虽然支持了struct、map、array类型来加强对半结构化数据类型的支持,但是这些类型都需要预定义schema,在数据导入、schema变更时有比较大的使用成本。

而Json类型由于其灵活多变,不需要预定义schema,为数据采集、数据分析、schema变更提供了极大的灵活性。在业务变动频繁的互联网,尤其是游戏行业,Json类型被广泛采用。其灵活性可以快速满足业务上对字段的增删要求。此外,Json作为跨语言的协议,也比较好地被各种大数据平台、中间件所支持。

今天,我们邀请质变科技AI-ready数据云团队布道师文军与大家分享话题《如何做好半结构化数据分析》。

挑战

Json类型应用起来比较灵活是因为Json逐行保存了schema信息。以github events数据集(https://www.gharchive.org)为例,每行数据都会重复存储key字段(id、type、actor、payload等)。Json的这种特性对数据仓库来说会带来如下三方面的挑战:

1)Json类型的每行数据都会反复存储key字段信息,导致较大的存储空间

2)通过数据仓库分析Json类型的数据时,需要逐行解析Json结构,占用大量的CPU

3)Json类型作为一个数据仓库表的一个列,作为IO的最小逻辑单元,无法在分析时按需加载Json子字段来减少IO,同时Json子字段不具备索引、裁剪的能力。

github events数据数据样例:

{"id":"2489651045","type":"CreateEvent","actor":{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":{"ref":"master","ref_type":"branch","master_branch":"master","description":"Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T15:00:00Z"}

面对此类问题,simdjson(https://github.com/simdjson/simdjson)被广泛应用在Json解析加速。相比于naive的实现,simdjson往往能带来10~20x的解析提升。但是,从端到端的视角来看,Json分析查询的耗时主要由Scan + Projection算子组成。simdjson一定程度上解决了projection算子的性能问题,当Json内容较大时,巨大的IO开销让查询的延迟变得不可接受。

Relyt JsonB类型

为了提升Json类型的分析性能、存储密度,Relyt AI-ready Data Cloud新增了对JsonB类型的支持。相比较于Json类型,Relyt在写入JsonB类型时,会对Json字符串进行解析,自动识别schema,并对数据进行结构化存储。从而,用户对Json子字段进行分析时,可以达到scalar类型接近的性能。用户在写入、分析JsonB类型的数据时,完全兼容Json类型的行为。

使用介绍

-- 创建包含JsonB字段的表

CREATE TABLE json_test (f0 json) 

  WITH(json_expand=on);

-- 写入Json数据

INSERT INTO json_test

VALUES

    ('{"key1": "value1", "key2": 1234, "key3": true} ');

-- 查询Json的子字段

-- ->>操作符

select f0->>'key1' from json_test;

-- json_extract_text函数

select json_extract_text(f0, '$.[key1]') from json_test;

实现原理

JsonB结构化存储

Relyt在处理Json类型的数据时,会对实际写入的Json数据内容进行统计,统计Json中Json key的出现频率、Json value的类型。在数据持久化到磁盘之前,Relyt会根据统计信息动态决策,哪些Json key需要结构化存储以及Json value的存储类型。Json key以列存文件的schema信息,不会在文件的数据区存储。Json value按照如下的类型转换关系映射成Relyt的类型:

Json value类型

Relyt类型

Json array

Array

Json object

Struct(not support yet)

Json number(整型)

Bigint

Json number(浮点类型)

Json

Json string

Varchar

Json boolean

Boolean

对于多行Json数据,相同Json key所对应的Json value类型不一致的情况。Json value的类型统一归类为Json类型。对于动态决策后不需要结构化存储的所有Json key,Relyt会将其合并成一个字飞来段,Json value的类型归类为Json类型。

样例一:

写入的Json数据

{"key1": "value1", "key2": 1234, "key3": true}
{"key1": "value2", "key2": 4321, "key3": "true"}

由于第一行Json数据的key3的Json value为Json boolean类型,第二行Json数据的key3的Json value为Json string类型。因而,结构化存储时,key3的类型映射为Json类型。

图一

样例二:

写入的Json数据

{"key1": "value1", "key2": 1234, "key3": true}
{"key1": "value2", "key2": 4321}
{"key1": "value3", "key2": 1000}
 

由于Json key "key3"的存在率未超过50%,因而key3并不进行结构化存储。

图二

JsonB查询优化

由于JsonB类型已经被结构化存储,Relyt在查询JsonB的数据时,可以只加载查询涉及的Json key数据。与此同时,也可以在Relyt存储类型与json_extract函数的返回类型一致的情况下,也可以进一步节省掉projection算子部分的计算消耗。下面分别从IO裁剪、projection视角来介绍Relyt在查询阶段的优化。

IO裁剪

select f0->>'key1' from json_test where f0->>'key1' = 'value2';

以上述SQL为例,Relyt在生成执行计划的时候,会识别->>操作符的有操作数。由于column f0已经被结构化存储了。因而,在生成执行计划的时候,仅仅Scan算子仅仅需要扫描,'key1'子字段。而'key1'、'key2'、'key3'这些子字段,Relyt是用列存来存储的,因而扫描的时候,可以做到只扫描'key1'子字段,从而做到纵向维度的IO裁剪。

此外,'key1'对于列存文件来说与普通的列没有区别。该列上的统计信息、索引等能力,Relyt在查询的时候,依然可以使用,从而达到横向维度的IO裁剪。

图三
 

Projection pushdown

如图三所示,针对JsonB的查询,Projection算子的json_extract_text函数优化掉了。因为'key1'的存储类型为Varchar,而json_extract_text的返回类型也是Varchar。不需要再进行额外的Projection计算。而如果'key1'存储的类型为bigint,与json_extract_text的返回类型Varchar不一致的情况下,还是需要额外的cast操作来对类型进行转换。这部分cast操作,我们需要pushdown到Scan算子中进行,因为不同的列存文件之间的JsonB字段的schema可能是不一致的。所以,我们需要文件粒度去决策是否要进行cast操作的计算。

图四
 

查询效果评估

JsonB类型在Relyt的核心客户上得到了很好的应用,下面的性能数据来自于该客户业务场景下的应用情况。在相同业务数据情况下,分别采用大宽表、JsonB、Json的方案的性能结果。

图五

 业界相关工作

Doris、Clickhouse都支持了variant类型来解决json数据格式的动态类型的问题,在内存中结构化表示json数据,避免了对json子字段访问的开销。磁盘上都通过高密度的列存来结构化存储json数据。

而spark社区目前也支持了variant类型,同时也通过variant shredding功能来做到列存的方式结构化存储json的子字段从而进一步提升IO裁剪能力。

 未来工作

Relyt AI-ready Data Cloud​​​​​​​当前仅支持Json第一级子字段的结构化存储,由于多级嵌套子字段在业务中还是比较场景的,这部分的支持我们还在进行中。

随着Spark和数据湖生态对Variant类型支持的完善,Relyt也会在目前湖仓一体的架构下做好对数据湖生态的Variant类型的支持,进一步强化数据湖的非结构化数据处理能力。

 相关引用

https://www.alibabacloud.com/help/zh/hologres/user-guide/json-and-jsonb-data-types?spm=a2c63.p38356.0.0.5fd6cf7409IzlM

https://doris.incubator.apache.org/zh-CN/docs/dev/sql-manual/sql-data-types/semi-structured/VARIANT

https://docs.databricks.com/en/semi-structured/variant-json-diff.html

https://www.databricks.com/blog/introducing-open-variant-data-type-delta-lake-and-apache-spark

https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md

https://github.com/simdjson/simdjson

https://www.gharchive.org

https://mp.weixin.qq.com/s/KVNcd25gngbBESU1QCYxbg

https://github.com/apache/spark/blob/master/common/variant/shredding.md

https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md#variant-data-in-parquet

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值