共计 13902 个字符,预计需要花费 35 分钟才能阅读完成。
作者:王华峰(花名继儒),Hologres 研发
近年来,随着挪动端利用的遍及,利用埋点、用户标签计算等场景开始诞生,为了更好的撑持这类场景,越来越多的大数据系统开始应用半结构化 JSON 格局来存储此类数据,以取得更加灵便的开发和解决。Hologres 是阿里云自研的云原生一站式实时数仓,反对 PB 级数据多维分析(OLAP)以及高并发低提早的在线数据服务(Serving),在对半结构化数据分析场景,Hologres 继续优化技术能力,从最开始反对 JSONB 类型,到反对 JSONB GIN 索引,再到 1.3 版本反对 JSONB 列存,在不就义应用灵活性的前提下,晋升 JSONB 数据的写入和查问性能,同时也升高存储老本。JSONB 列存也在阿里团体外部多个外围业务应用,其中稳固撑持搜寻事业部 2022 年双 11 大促,历经生产考验,查问性能晋升 400%,存储降落 50%!
点击查看阿里巴巴搜寻事业部双 11JSONB 实际 >> 降级 JSONB 列式存储,Hologres 助力淘宝搜寻 2022 双 11 降本增效!
通过本文,咱们将会揭秘 Hologres JSONB 半结构化数据的技术原理,实现 JSON 半构造数据的极致剖析性能。
什么是半结构化数据
介绍什么是半构造数据之前,咱们首先明确下什么是结构化数据。结构化数据能够了解成在关系型数据库(RDBMS)中的一张表,每张表都有明确严格的构造定义,比方蕴含哪些列,每列的数据类型是怎么的,存储的数据必须严格遵循表构造的定义。
绝对应的,半结构化数据就是非固定构造的、常常变动的,且个别是自描述的,数据的构造和内容混淆在一起,最典型的例子就是 JSON 格局数据。JSON 有规范的格局定义,其次要由对象(Object)和数组形成(Array),对象中存储的是键值对,其中键只能是字符串,值能够是字符串、数组、布尔值、Null 值、对象或者数组,数组中能够寄存任意多个值。
以下就是一个简略的 JSON 实例,置信大家都很相熟:
{"user_name": "Adam", "age": 18, "phone_number": [123456, 567890]}
Hologres 以后正是通过反对 JSON 数据类型来提供半结构化数据的能力,为了兼容 Postgres 生态,咱们反对 Postgres 的 JSON/JSONB 这两种原生类型,其中 JSON 类型理论以 TEXT 格局进行存储,而 JSONB 类型存储的是解析过后的二进制,因为查问时不须要再解析,所以 JSONB 在解决时会快很多,下文提到的 Hologres 半结构化数据计划的很多外部优化都是依靠 JSONB 类型实现的。
咱们为什么须要半结构化数据?
半结构化数据得益于其自身的易用性以及弱小的表达能力,使得半结构化数据的应用场景十分宽泛。
对于数仓来说,每当上游的数据格式有变更时,比方变更数据类型、增删字段,数仓中的强 Schema 格局的表,必须进行相应的表构造演进(Schema Evoluation)来适配上游的数据,比方须要执行 DDL 进行加列或者删列,甚至两头的实时数据 ETL 作业也须要进行适配改变并从新上线。
在有频繁 Schema Evoluation 的场景的时候,如何保证数据的品质是个很大的挑战,同时保护和治理表构造,对于数据开发人员来说也是一项琐碎且麻烦的工作。
而半结构化数据则人造反对 Schema Evoluation,上游业务的变更,只须要在 JSON 列数据中进行增删相应的字段,无需对数仓中的表做任何 DDL 就能实现,也能对两头的 ETL 作业做到通明,这样就能大大降低保护和治理表构造的老本。
传统数仓的半结构化数据解决方案
数仓在解决半结构化数据的时候,掂量一个解决方案好坏的外围考量次要有两点:
- 是否放弃半结构化数据的易用性和灵活性
- 是否实现高效的查问性能
而传统的解决方案经常是顾此失彼,没法做到“熊掌”与“鱼”的兼得。常见的 JSON 数据处理形式有 2 种:
以下计划都以 JSON 数据为例,假如咱们有如下 JSON 数据:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16} |
---|
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41} |
{“user_id”:1003, “user_name”: “Clair”, “gender”: “Female”, “age”: 21} |
计划 1: 数仓间接存储原始 JSON 数据
一种最直观的计划就是将原始 JSON 数据存成独自的一列,以 Hive 为例:
在存储层,这张 Hive 表的数据也是以一个残缺的 JSON 值作为最小的存储粒度在磁盘上间断存储:
之后应用相干的 JSON 函数进行查问,比方查问所有年龄大于 20 的用户数:
SELECT COUNT(1) FROM tbl WHERE cast(get_json_object(json_data, '$.age') as int) > 20;
形象成上面的流程:
上游间接写入 JSON 类型到 Hologres,两头不通过解决,应用层查问时,再去解析须要的数据。
这种解决形式:
- 长处是:JSON 则人造反对 Schema Evoluation,上游业务的变更,只须要在 JSON 列数据中进行增删相应的字段,无需对数仓中的表做任何 DDL 就能实现,也能对两头的 ETL 作业做到通明,最大水平地保留了半结构化数据的易用性和灵活性,能大大降低保护和治理表构造的老本。
- 毛病是:利用端查问时须要抉择适合的处理函数和办法,能力解析到须要的数据,开发较为简单,如果 JSON 较简单,同时查问性能会有进化,因为每次 JSON 列的数据参加计算的时候,都须要对 JSON 数据残缺的解析一遍,比方须要抽取出整个 JSON 中某个字段,那么查问引擎执行的时候就要读出每一行 JSON,解析一遍,取出须要的字段再返回。这两头会波及大量的 IO 和计算,而须要的可能只是 JSON 数据成千盈百字段当中的一个字段,这两头的大量 IO 和计算都是节约的。
计划 2: 加工成宽表
既然 JSON 查问时的解析开销很大,那就把解析前置在数据加工链路中,于是另外一种做法就是把 JSON 拍平成了一张宽表:
相应的形象进去的流程如下:
上游是 JSON 格局,在导入时,将 JSON 进行解析,比方常见的通过 Flink 的 JSON_VALUE 函数解析,而后打宽成一张大宽表,再写入至 Hologres,对于下层利用,间接查问 Hologres 中曾经解析好的列。
对于这种解决办法:
- 长处是:写入 Hologres 时,因为是一般列写入,所以写入性能会更好,同时在查问侧,不须要对 JSON 数据进行解析,查问性能也会更好。
- 毛病是:每当上游的数据格式有变更时,比方变更数据类型、增删字段、执行 DDL 进行加列或者删列,两头的实时数据 ETL 作业也须要进行适配改变并从新上线,应用十分不灵便,也会额定减少运维和开发累赘。
基于此背景,业界也迫切需要一个既能放弃高效的查问性能,又不就义应用灵活性的计划,来应答海量半结构化数据的极致剖析场景。
Hologres 列式 JSON 实现计划
为了更好的反对 JSON 剖析场景,Hologres 一直迭代技术能力,在晚期版本反对了 JSON 数据格式和相干解析函数,用户能够间接写入 JSON 类型以及相干的查问解析。同时 1.1 版本在查问层做了 JSON 相干的优化,无效的晋升 JSON 数据查问性能,比方反对 GIN 倒排索引,减速 JSON 数据的过滤,反对表达式下推等,但整体减速场景无限且应用难度较高,于是 1.3 版本咱们做了大量的存储层优化,通过 JSONB 列存的形式来实现更好的查问性能。
总体方案介绍
经咱们察看,理论用户的非结构化数据,在一段时间周期内,整体数据的构造都是比较稳定的,通常只会有无限个数的确定的字段,区别只是每个字段呈现的频率会有所不同,且每个字段的数据类型也是整体稳固的。
基于以上教训,Hologres 提供的实现计划的外围要点就是,在导入 JSON 类型数据至 Hologres 的时候,引擎主动去抽取 JSON 数据的构造(字段个数,字段类型等),而后在存储层,将 JSON 数据转化成强 Schema 格局的列式存储格局的文件,以此来达到减速查问的成果,同时对外接口上,仍旧放弃 JSON 的语义,真正做到了放弃 JSON 易用性的同时,兼顾了 OLAP 查问性能。
以下图为例,Hologres 每张表在同一个 Shard 上的数据,也是会分文件存储的,而且同一个文件中的数据,通常也是在邻近的工夫点写入的,所以在 JSON 场景下,文件与文件之间可能会有构造的差别,但单个文件内的数据能有比较稳定的构造,从而整体上做到 JSON 数据结构的稳固演进。
JSON 与 JSONB
在具体介绍 Hologres JSON 列存化实现之前,咱们先简略介绍下 Postgres 中的 JSON 和 JSONB 两种数据类型的区别。
JSON 和 JSONB 这两种数据类型在用户接口上没有很大的差别,大部分操作符都是雷同,次要区别在于存储格局上的差异:
- JSON 类型只会校验写入的数据是否合乎 JSON 标准,存储上间接将 JSON 原文依照 TEXT 存储,无任何优化
- JSONB 在 JSON 的根底上,会对数据进行格局优化,存储的是对原始 JSON 数据优化过后的二进制格局,其优化蕴含但不限于:
<!—->
-
- 去除数据中的冗余空格
- 对雷同门路下的同名字段去重
- 对 JSON 数据中的字段进行排序,重新排列组织,减速查问能力
在函数笼罩上,JSON 和 JSONB 这两个类型也有些许差异,比方 JSON 类型无奈间接 Cast 成 INT/Float/Numeric 等类型,而 JSONB 则能够,所以整体语法层面 JSONB 更残缺易用。
Hologres 的 JSON 列存化计划,以后的实现次要还是基于 JSONB 这个数据类型,具体起因下文会讲到。
JSON 构造抽取
JSON 数据的构造抽取,次要做的是确定 JSON 数据的格局,包含 JSON 具体有哪些字段,每个字段对应的数据类型,以此作为底层列存文件的理论存储构造。
Hologres 数据写入流程整体是个 LSM (The Log-Structured Merge-Tree)架构,当数据写入到 Hologres 的一张表的时候,数据首先会写到内存表(MemTable) 中,当一个 MemTable 满了当前,将其以异步的形式 Flush 到文件系统中(下图第 4 步),并初始化一个新的 MemTable,同时后盾会有工作,不停将 Flush 到文件系统的文件做进一步的合并(Compaction,下图第 5 步),更多详情见 Hologres 存储引擎揭秘
而 JSON 数据的构造抽取,也次要产生在 Flush 和 Compaction 两个阶段。
Flush 阶段
当 MemTable Flush 时,咱们会遍历一次在 MemTable 中所有 JSON 数据,记录下每个 JSON 中呈现过的字段,以及每个字段的数据类型,遍历实现后,就能晓得这列 JSON 数据列存化之后,具体会有哪些列以及每一列的对应类型。
还是以上面的数据为例:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16} |
---|
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41} |
{“user_id”:1003, “user_name”: “Clair”, “gender”: “Female”, “age”: 21} |
咱们就可能抽取出以下 JSON 格局:
列名 | 数据类型 |
---|---|
user_id | INT |
user_name | TEXT |
gender | TEXT |
age | INT |
另外,在遍历 JSON 的过程中,咱们也会进行类型泛化。比方 user_id 字段某一行数据呈现了超过 INT 类型阈值的值,咱们就会把 user_id 列的类型泛化成 BigINT 类型来兼容所有数据。
抽取完 JSON 构造之后,咱们就能把 MemTable 中的数据写到文件系统了,JSON 列数据会被拆分写到对应的 4 列中去。
Compaction 阶段
Compaction 做的事件就是把多个文件合并成一个更大的文件,这里也波及到 JSON 构造的抽取。
与 Flush 不同的是,因为 Compaction 的输出文件曾经对 JSON 列进行了列存化解决,所以咱们在大部分状况下并不需要再残缺遍历所有文件中的 JSON 数据去抽取构造,而是能够间接通过文件的 Meta 信息就能推导出输入文件的 JSON 格局,只须要对所有文件的输出列取一个并集,并对抵触列的类型进行泛化即可。
通过上述 Flush 和 Compaction 阶段的 JSON 数据处理,咱们就能将数据在存储层列式化,便于后续的查问减速。
查问自适应改写
上文提到,Hologres 尽管底层存储将 JSONB 数据转成了列式存储,但用户接口还是沿用了原生 JSONB 的查问接口,而因为底层 JSONB 数据格式的扭转,如果查问引擎还是将列式化后的数据当成 JSONB 类型,查问势必会失败(数据的理论输出类型和执行打算的预期输出类型不统一),所以这就要求咱们的查问引擎有查问自适应改写的能力。
接下来咱们以一个简略的 SQL 为例子解说查问过程中波及到的查问自适应改写:
CREATE TABLE TBL(json_data jsonb); -- 建表 DDL
SELECT SUM((json_data->'quantity')::BIGINT) FROM TBL;
在 Hologres 中,对 JSONB 类型最罕用的两个操作符就是-> 和->>
- -> 操作符的含意是,依据操作符前面的门路参数,取出对应的 JSONB 数据,该操作符的返回数据类型是 JSONB
- ->> 操作符的含意是,依据操作符前面的门路参数,取出对应的 JSONB 数据,该操作符的返回数据类型是 TEXT
所以,下面例子的含意就是,读取 json_data 这一 JSONB 列中的 quantity 字段,并转成 BIGINT 类型后,进行 SUM 聚合运算。
所以在物理执行打算中,Scan 节点就会有上图中最右边的表达式树,根节点代表将 JSONB 转换成 BIGINT 的函数,它的孩子节点表是取出 json_data 列中的 quantity 字段。
但实际上底层文件存储的是列存化后的数据,曾经没有了 json_data 这一物理 JSON 列,所以咱们在 Scan 节点就须要进行自适应的物理执行打算改写:
- 第一步就是进行列裁剪,如果咱们发现底层文件的 Meta 信息中含有 quantity 这一列,咱们就能够间接打消 -> 这一表达式计算,失去了上图两头所示的表达式树。当然如果咱们发现 Meta 信息中没有quantity 这一列,那咱们就能够间接跳过扫描这个文件,返回执行后果,大大晋升执行效率。
- 第二步就是依据文件 Meta 信息判断 quantity 这一列的物理存储类型,当咱们发现理论存储类型和要求 Cast 的类型指标统一时,咱们就能进一步改写优化执行打算,省去了 Cast 的操作,失去了上图中最右所示的表达式树,也就是间接返回物理存储的列数据。另外如果理论存储类型是 INT,那么咱们就须要将原始的 Cast 节点替换成 INT 到 BIGINT 的 Cast 操作,来保障后果的正确性。
那为什么不间接让 SQL Optimizer 把执行打算一开始就改写好呢?
起因在于优化器并不知道 JSONB 列在存储引擎的真正格局,比方同一列quantity,在文件 A 中的类型是 INT,在文件 B 中的类型是 TEXT,所以对于不同文件的执行打算可能是不同的,SQL Optimizer 无奈用一个物理执行打算表白所有可能的状况,这就要求执行引擎可能进行自适应的执行打算改写。
脏数据、稠密数据处理
因为 JSON 类型的易用性,实践上用户能够写入任意合乎 JSON 格局的数据,这也导致相较于强 Schema 类型,JSON 类型更容易产生脏数据,这就要求 Hologres 的 JSON 列式计划要有比拟强的鲁棒性,可能容忍脏数据,这里咱们次要探讨两类问题::数据类型不统一的问题以及字段名谬误导致的数据稠密问题。
脏数据
首先如何解决不统一的数据类型,假如咱们当初有以下 JSON 数据须要列式存储:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16} |
---|
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41} |
{“user_id”:1003, “user_name”: “Claire”, “gender”: “Female”, “age”: “21”} |
能够看到 age 列的前两行数据都是 INT 类型的,然而到第三行的时候,age 列的值就是一个 TEXT 类型的数据了,这时候咱们就会对类型泛化,泛化成咱们在上文提到 JSONB 类型:
列名 | 数据类型 |
---|---|
user_id | INT |
user_name | TEXT |
gender | TEXT |
age | JSONB |
咱们能够把 JSON 看做是个递归定义的格局,像 16、41、”21″ 这些 age 字段的值,自身也是一个 JSON 值(Object 类型),所以咱们能够进行这样的类型泛化。这样泛化之后,之后对于 age 列的查问性能会稍弱于没有脏数据的状况,因为在执行引擎层,无奈像上一节提到的,间接略去 JSONB 的 Cast 操作,但整体性能还是远好于没有 JSON 列存化的计划的,因为咱们还是只须要读取 age 这一列数据,能够省去大量的 IO 和计算操作。
稠密数据
咱们再来看下如何解决稠密数据,通常稠密数据产生的起因是上游数据生成的逻辑有问题,生成了大量不反复的字段名,比方以下数据:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16, “key_1”: “1”} |
---|
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41, “key_2”: “2”} |
{“user_id”:1003, “user_name”: “Claire”, “gender”: “Female”, “age”: 21, “key_3”: “3”} |
能够看到每一行都有一个不一样的字段,且不反复,如果咱们抉择抽取 key_1,key_2,key_3 这三列,那这三列的数据就会十分稠密,也会导致整体文件的列数收缩的很厉害。
咱们抉择将这些稠密数据独自抽取到非凡的一列(holo.remaining),该列的类型也是 JSONB,咱们会把呈现频度低于某个阈值(可配置)的数据,都寄存到这个字段中:
列名 | 数据类型 |
---|---|
user_id | INT |
user_name | TEXT |
gender | TEXT |
age | INT |
holo.remaining | JSONB |
能够认为在 remaining 列中存储的就是整个 JSON 数据的一个子集,这一列并上其余列式化的数据,就能结构成原来残缺的一个 JSON 值。
查问 remaining 列时的性能也会稍弱于查问曾经列式化的列,因为存储的是 JSONB,会蕴含所有稠密字段,所以查问时须要在 JSONB 数据中搜寻指定的字段,这里有额定的开销。但因为这一列中存储的都是稠密的数据,通常查问命中 remaining 列的概率也不会很高,所以能够容忍。
嵌套与简单构造解决
上文中给出的 JSON 实例都是比较简单的扁平化的数据,但实际上含有嵌套构造的 JSON 数据也是比拟常见的,接下来简略介绍下 Hologres 是如何解决简单 JSON 构造的。
嵌套构造
对于嵌套构造,咱们能够把 JSON 数据看成是一颗树,数据都存在叶子节点中(没有简单嵌套构造的状况下),比方上面这个 JSON 数据,就会抽取出右图所示的树形构造:
因为非叶子节点自身并不存储数据,所以实际上存储的时候就能够把下面的树状构造拍平失去以下表构造,另外咱们的元数据会记录节点的深度信息,以此来保障拍平的时候不会呈现列名歧义或者抵触的状况。
简单嵌套构造
首先咱们须要先明确下以后 Hologres 抽取 JSON 构造时,只会抽取出以下根本类型:
- INT
- BIGINT
- TEXT
- INT[]
- BIGINT[]
- TEXT[]
- JSONB
这外面 JSONB 类型就是咱们尝试类型泛化后仍旧无奈抽取成后面 6 种根本类型时,作为兜底的类型实现,这当中也包含的简单嵌套构造,比方上面这行 JSON 数据就会抽取出右图所示的构造,能够看到对于 descs 这个字段,因为是数组外面嵌套了非根本类型数据,所以这里类型进化成了 JSONB 类型。
所以这里要留神的点就是,对于这类进化成 JSONB 类型的数据,针对这一列的操作的性能会不如那些抽成根本类型数据的列,但整体性能还是会比非列式 JSON 计划会好很多,因为 JSONB 列只存储了残缺 JSON 数据的一个子集,查问这一列波及到的 IO 和计算都会小很多。
列式 JSON 不实用场景
查问带出残缺 JSON 数据
Hologres 的列式 JSON 计划对于大部分应用场景都有比拟好的优化成果,次要须要留神的点是,对于查问后果须要带出残缺 JSON 列的场景,性能相较于间接存储原始格局的 JSON 会有进化,比方以下 SQL:
CREATE TABLE TBL(pk int primary key, json_data jsonb); -- 建表 DDL
SELECT json_data FROM TBL WHERE pk = 123;
SELECT * FROM TBL limit 10;
起因在于底层曾经将 JSON 数据转成了列式存储,所以当须要查问出残缺 JSON 数据的时候,就须要将那些曾经列式存储的数据从新拼装成原来的 JSON 格局:
这个步骤就会产生大量的 IO 以及转换开销,如果波及到的数据量很大,列数又很多,甚至可能成为性能瓶颈,所以这种场景下倡议不要开启列式优化。
极稠密的 JSON 数据
上文曾经提到,当咱们列式化 JSON 数据遇到稠密的字段时,咱们会将这部分字段合并至一个叫做 holo.remaining 的非凡列中,以此来防止列数收缩的问题。
所以如果用户的 JSON 数据,蕴含的都是稠密字段,比方极其状况下每个字段都只会呈现一次,那么咱们的列式化将不会起效,因为所有字段都是稠密的,那么所有字段都会合并至 holo.remaining 字段,等于没有进行列式化,这种状况下就不会有查问性能的晋升。
Hologres 列式 JSON 计划收益:降本增效
收益 1:存储降本
咱们应用了 TPCH 的数据集来测试 Hologres JSON 列式计划对于存储空间的优化成果,具体测试比照计划是将 TPCH 的表都建成一列 JSONB 的格局,而后比照开启列式计划的成果(几张数据量较小的表略去了):
-- 存储原始 Jsonb 数据的表
CREATE TABLE CUSTOMER(data jsonb);
CREATE TABLE LINEITEM(data jsonb);
CREATE TABLE ORDERS(data jsonb);
CREATE TABLE PART(data jsonb);
CREATE TABLE PARTSUPP(data jsonb);
-- 开启列式 Json 优化的表
CREATE TABLE CUSTOMER_COLUMNAR(data jsonb);
ALTER TABLE CUSTOMER_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);
CREATE TABLE LINEITEM_COLUMNAR(data jsonb);
ALTER TABLE LINEITEM_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);
CREATE TABLE ORDERS_COLUMNAR(data jsonb);
ALTER TABLE ORDERS_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);
CREATE TABLE PART_COLUMNAR(data jsonb);
ALTER TABLE PART_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);
CREATE TABLE PARTSUPP_COLUMNAR(data jsonb);
ALTER TABLE PARTSUPP_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);
应用了 TPCH 100GB 的测试集进行验证,后果如下:
表名 | 原始 JSONB 存储 (GB) | 开启列式 JSONB 优化存储(GB) | 数据压缩比 |
---|---|---|---|
CUSTOMER | 4.4 | 2.14 | 2.06 |
LINEITEM | 43 | 18 | 2.39 |
ORDERS | 14 | 4.67 | 3 |
PART | 5.2 | 1.3 | 4.3 |
PARTSUPP | 7.8 | 5.8 | 1.34 |
能够看到,开启列式 JSONB 优化后,每张表的存储空间都有比较显著的降落,起因在于列式化之后:
- 原来 JSON 数据中的字段名都不会再存储了,而只须要存储每个字段对应的具体值,比方上面是转成 JSON 后 CUSTOMER 表的一行数据,数据中的 c_name、c_phone、c_acctbal 等字符串,列式化后都不需存储
{"c_name": "Customer#002662050", "c_phone": "23-793-162-6786", "c_acctbal": 4075.57, "c_address": "paJBRFkD N368pMSvGsYivWyRAs", "c_comment": "ly. fluffily even packages along the blithely even deposits should sleep slyly above the", "c_custkey": 2662050, "c_nationkey": 13, "c_mktsegment": "BUILDING"}
- 列式化后每列的数据类型都是一样的,列式存储能有比拟好的数据压缩率
这里要多说一点的是,在某些数据集上咱们也察看到过开启列式优化后理论存储空间没有降落的状况,这种状况通常是因为 JSON 数据中的字段比拟稠密,列数收缩比拟厉害,且列式化后每一列的类型都是 TEXT 类型,导致压缩成果不好导致的。所以上述测试只是一个理论值,理论用户的数据各种各样,理论压缩后的存储成果还是要以理论状况为准。
收益 2:查问性能晋升
得益于底层列式化的存储格局,对于那些可能利用到 JSON 列裁剪的查问,经咱们测试察看,通常性能都会有数倍的晋升,甚至在特定场景下能有十倍以上的性能晋升。
这里咱们应用 Github 的数据集(见文末 SQL 和 DDL 附录)来验证 Hologres JSON 列式化计划的查问晋升,该数据集记录了 Github 上的各种用户行为日志,包含发动代码评审、评论等等,该数据集是一份 JSON 格局的数据集。咱们选用了 2015 年的总计 172309645 行的数据,导入到同一个 Hologres 实例后,比照了应用原生 JSON 类型、原生 JSONB 类型存储和开启列式 JSONB 优化后的查问性能:
查问 | JSON(ms) | JSONB(ms) | 列式 JSONB(ms) | 列式 JSONB 相较于原生 JSONB 的性能晋升 |
---|---|---|---|---|
Query1 | 26426 | 15831 | 50 | 31562% |
Query2 | 28657 | 19285 | 320 | 5926% |
Query 3 | 26900 | 17062 | 869 | 1863% |
Query 4 | 57356 | 44314 | 1430 | 2999% |
能够看到,开启列式 JSONB 优化后的查问性能,相较于原始 JSONB 格局,有了质的晋升。但要留神的是,因为数据集的不同,以及查问模式的不同,性能收益可能会有较大的差别,具体成果还是要以理论状况为准。后续咱们也将陆续推出 Hologres JSON 列式计划在不同场景下的实现案例,以及对应的性能收益。
淘宝搜寻举荐 A / B 试验场景胜利案例
阿里巴巴搜寻举荐事业部通过 Hologres 承载了阿里巴巴团体淘宝、淘宝特价版、饿了么等多个电商业务的实时数仓场景,包含即席多维分析,A/B Test 等。
在搜寻举荐这类业务场景中,会有很多的用户标签、商品标签、卖家标签和算法桶号等多值属性,以用户标签为例,业务上对用户的画像属性不是变化无穷的,业务可能随时须要新增一类属性进行观测,如果每次都须要用一个新的字段来存储新的用户属性,那在整个实时链路上都会非常低效,在应用列式 JSONB 之前,应用的是 Text 数组类型作为多值字段的存储格局。
以下是数据和查问示例:
上述 SQL 的含意就是过滤出命中 ’layerA:1’、’layerA:2’ 这两个分桶的数据,并计算对应的 PV。
该计划曾经稳固应用了几年,但该计划并不是最高效的,无论是存储老本还是计算性能都有进一步晋升的空间,且咱们认为,从整个数据模型来说,应用 JSONB 来存储各种属性,才是最直观天然的形式。
以下是改成 JSONB 计划 后的数据与查问示例:
second_timestamp | pk | UID | … | bts_tags |
---|---|---|---|---|
2022-11-11 00:00:00+08 | 858739e966f7ebd1cfaa49c564741360 | 1 | {“layerA”:”1″, “layerB”:”11″, “layerC”:”111″} | |
2022-11-11 00:00:01+08 | e7e3d71fac5a92b87c3278819f6aff8c | 2 | {“layerA”:”1″, “layerB”:”12″, “layerC”:”112″} | |
2022-11-11 00:00:01+08 | 828f07dc16f4fa2f4be5ba3a9d38f12a | 3 | {“layerA”:”2″, “layerB”:”11″, “layerC”:”111″} |
SELECT
'layerA:' || (bts_tags ->> 'layerA') AS "bts_tags",
COALESCE(sum(scene_count), 0) AS "pv"
FROM
wireless_pv
WHERE
second_timestamp >= TIMESTAMPTZ '2022-11-11 00:00'
AND second_timestamp <= TIMESTAMPTZ '2022-11-11 23:59'
AND bts_tags ->> 'layerA' IN ('1', '2')
GROUP BY
'layerA:' || (bts_tags ->> 'layerA');
能够看到,切换成 JSONB 后,无论是数据还是查问,都更加直观且天然了,2022 年双 11 实现了 Hologres 列式 JSONB 计划的迁徙,并且在迁徙后,无论是存储老本还是查问性能,都取得了十分不错的收益:
表名 | Array 格局查问均匀提早(ms) | JSONB 格局查问均匀提早(ms) | JSONB 性能晋升 |
---|---|---|---|
wireless_pv | 5975.5 | 1553.8 | 280% |
wireless_dpv | 862.5 | 455.8 | 89% |
表名 | Array 格局存储量 | JSONB 格局存储量 | 存储降落 % |
---|---|---|---|
wireless_pv | 35 TB | 15 TB | -57% |
wireless_dpv | 3472 GB | 1562 GB | -55% |
更多具体细节,请参阅: 降级 JSONB 列式存储,Hologres 助力淘宝搜寻 2022 双 11 降本增效!
总结
Hologres 的列式 JSON 计划,真正做到了在放弃 JSON 易用性和灵活性的同时,兼顾了极致的 OLAP 查问性能,让用户可能在 Hologres 上充沛开掘半结构化数据,甚至让 Hologres 这个一站式实时数仓承当局部数据湖的能力。咱们后续也会持续一直优化列式 JSON 实现,为大家带来更为极致的性能,敬请期待。
附录
- Github 数据集: https://www.gharchive.org/
- 查问性能测试 DDL
CREATE TABLE gh_2015(gh_jsonb jsonb);
-- 开启列式优化
ALTER TABLE gh_2015 ALTER COLUMN gh_jsonb SET (enable_columnar_type = on);
- 查问性能测试 Query
--Query 1
SELECT COUNT(1) FROM gh_2015 WHERE gh_jsonb->>'type' = 'WatchEvent';
--Query 2
SELECT gh_jsonb->'repo'->>'name', count(1) AS stars FROM gh_2015 WHERE gh_jsonb->>'type' = 'WatchEvent' GROUP BY gh_jsonb->'repo'->>'name' ORDER BY stars DESC LIMIT 50
--Query 3
SELECT to_date((substring((gh_jsonb ->> 'created_at')FROM 1 FOR 8) || '01'), 'YYYY-MM-DD') AS event_month,
sum(coalesce((gh_jsonb -> 'payload' -> 'issue' ->> 'number'), (gh_jsonb -> 'payload' -> 'pull_request' ->> 'number'),
(gh_jsonb -> 'payload' ->> 'number'))::int) AS closed
FROM gh_2015
WHERE (gh_jsonb ->> 'type') = 'IssuesEvent'
AND (gh_jsonb -> 'payload' ->> 'action') = 'closed'
AND (gh_jsonb -> 'repo' ->> 'id')::bigint = 41986369
GROUP BY 1;
--Query 4
SELECT event_month,
all_size
FROM
(SELECT event_month,
COUNT(*) OVER (PARTITION BY event_month) AS all_size,
ROW_NUMBER() OVER (PARTITION BY event_month) AS row_num
FROM
(SELECT (gh_jsonb ->> 'type') AS TYPE,
(gh_jsonb -> 'repo' ->> 'id')::bigint AS repo_id,
(gh_jsonb -> 'payload' ->> 'action') AS action,
to_date((substring((gh_jsonb ->> 'created_at')
FROM 1
FOR 8) || '01'), 'YYYY-MM-DD') AS event_month
FROM gh_2015) t
WHERE TYPE = 'PullRequestEvent'
AND repo_id = 41986369
AND action = 'opened') sub
WHERE row_num = 1
ORDER BY event_month;
理解 Hologres:https://www.aliyun.com/product/bigdata/hologram