作者:王华峰(花名继儒),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作业做到通明,这样就能大大降低保护和治理表构造的老本。

传统数仓的半结构化数据解决方案

数仓在解决半结构化数据的时候,掂量一个解决方案好坏的外围考量次要有两点:

  1. 是否放弃半结构化数据的易用性和灵活性
  2. 是否实现高效的查问性能

而传统的解决方案经常是顾此失彼,没法做到“熊掌”与“鱼”的兼得。常见的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这两种数据类型在用户接口上没有很大的差别,大部分操作符都是雷同,次要区别在于存储格局上的差异:

  1. JSON类型只会校验写入的数据是否合乎JSON标准,存储上间接将JSON原文依照TEXT存储,无任何优化
  2. JSONB在JSON的根底上,会对数据进行格局优化,存储的是对原始JSON数据优化过后的二进制格局,其优化蕴含但不限于:

<!---->

    1. 去除数据中的冗余空格
    2. 对雷同门路下的同名字段去重
    3. 对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_idINT
user_nameTEXT
genderTEXT
ageINT

另外,在遍历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); --建表DDLSELECT SUM((json_data->'quantity')::BIGINT) FROM TBL;

在Hologres中,对JSONB类型最罕用的两个操作符就是->->>

  1. -> 操作符的含意是,依据操作符前面的门路参数,取出对应的JSONB数据,该操作符的返回数据类型是JSONB
  2. ->> 操作符的含意是,依据操作符前面的门路参数,取出对应的JSONB数据,该操作符的返回数据类型是TEXT

所以,下面例子的含意就是,读取json_data这一JSONB列中的quantity字段,并转成BIGINT类型后,进行SUM聚合运算。

所以在物理执行打算中,Scan节点就会有上图中最右边的表达式树,根节点代表将JSONB转换成BIGINT的函数,它的孩子节点表是取出json_data列中的quantity字段。

但实际上底层文件存储的是列存化后的数据,曾经没有了json_data这一物理JSON列,所以咱们在Scan节点就须要进行自适应的物理执行打算改写:

  1. 第一步就是进行列裁剪,如果咱们发现底层文件的Meta信息中含有quantity这一列,咱们就能够间接打消-> 这一表达式计算,失去了上图两头所示的表达式树。当然如果咱们发现Meta信息中没有quantity这一列,那咱们就能够间接跳过扫描这个文件,返回执行后果,大大晋升执行效率。
  2. 第二步就是依据文件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_idINT
user_nameTEXT
genderTEXT
ageJSONB

咱们能够把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_idINT
user_nameTEXT
genderTEXT
ageINT
holo.remainingJSONB

能够认为在remaining列中存储的就是整个JSON数据的一个子集,这一列并上其余列式化的数据,就能结构成原来残缺的一个JSON值。

查问remaining列时的性能也会稍弱于查问曾经列式化的列,因为存储的是JSONB,会蕴含所有稠密字段,所以查问时须要在JSONB数据中搜寻指定的字段,这里有额定的开销。但因为这一列中存储的都是稠密的数据,通常查问命中remaining列的概率也不会很高,所以能够容忍。

嵌套与简单构造解决

上文中给出的JSON实例都是比较简单的扁平化的数据,但实际上含有嵌套构造的JSON数据也是比拟常见的,接下来简略介绍下Hologres是如何解决简单JSON构造的。

嵌套构造

对于嵌套构造,咱们能够把JSON数据看成是一颗树,数据都存在叶子节点中(没有简单嵌套构造的状况下),比方上面这个JSON数据,就会抽取出右图所示的树形构造:

因为非叶子节点自身并不存储数据,所以实际上存储的时候就能够把下面的树状构造拍平失去以下表构造,另外咱们的元数据会记录节点的深度信息,以此来保障拍平的时候不会呈现列名歧义或者抵触的状况。

简单嵌套构造

首先咱们须要先明确下以后Hologres抽取JSON构造时,只会抽取出以下根本类型:

  1. INT
  2. BIGINT
  3. TEXT
  4. INT[]
  5. BIGINT[]
  6. TEXT[]
  7. 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); --建表DDLSELECT 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)数据压缩比
CUSTOMER4.42.142.06
LINEITEM43182.39
ORDERS144.673
PART5.21.34.3
PARTSUPP7.85.81.34

能够看到,开启列式JSONB优化后,每张表的存储空间都有比较显著的降落,起因在于列式化之后:

  1. 原来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"}
  1. 列式化后每列的数据类型都是一样的,列式存储能有比拟好的数据压缩率

这里要多说一点的是,在某些数据集上咱们也察看到过开启列式优化后理论存储空间没有降落的状况,这种状况通常是因为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的性能晋升
Query126426158315031562%
Query228657192853205926%
Query 326900170628691863%
Query 4573564431414302999%

能够看到,开启列式JSONB优化后的查问性能,相较于原始JSONB格局,有了质的晋升。但要留神的是,因为数据集的不同,以及查问模式的不同,性能收益可能会有较大的差别,具体成果还是要以理论状况为准。后续咱们也将陆续推出Hologres JSON列式计划在不同场景下的实现案例,以及对应的性能收益。

淘宝搜寻举荐A/B试验场景胜利案例

阿里巴巴搜寻举荐事业部通过Hologres承载了阿里巴巴团体淘宝、淘宝特价版、饿了么等多个电商业务的实时数仓场景,包含即席多维分析,A/B Test等。

在搜寻举荐这类业务场景中,会有很多的用户标签、商品标签、卖家标签和算法桶号等多值属性,以用户标签为例,业务上对用户的画像属性不是变化无穷的,业务可能随时须要新增一类属性进行观测,如果每次都须要用一个新的字段来存储新的用户属性,那在整个实时链路上都会非常低效,在应用列式JSONB之前,应用的是Text数组类型作为多值字段的存储格局。

以下是数据和查问示例:

上述SQL的含意就是过滤出命中'layerA:1'、'layerA:2'这两个分桶的数据,并计算对应的PV。

该计划曾经稳固应用了几年,但该计划并不是最高效的,无论是存储老本还是计算性能都有进一步晋升的空间,且咱们认为,从整个数据模型来说,应用JSONB来存储各种属性,才是最直观天然的形式。

以下是改成JSONB计划后的数据与查问示例:

second_timestamppkUID...bts_tags
2022-11-11 00:00:00+08858739e966f7ebd1cfaa49c5647413601{"layerA":"1", "layerB":"11", "layerC":"111"}
2022-11-11 00:00:01+08e7e3d71fac5a92b87c3278819f6aff8c2{"layerA":"1", "layerB":"12", "layerC":"112"}
2022-11-11 00:00:01+08828f07dc16f4fa2f4be5ba3a9d38f12a3{"layerA":"2", "layerB":"11", "layerC":"111"}
SELECT'layerA:' || (bts_tags ->> 'layerA') AS "bts_tags",COALESCE(sum(scene_count), 0) AS "pv"FROMwireless_pvWHEREsecond_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_pv5975.51553.8280%
wireless_dpv862.5455.889%
表名Array 格局存储量JSONB格局存储量存储降落%
wireless_pv35 TB15 TB-57%
wireless_dpv3472 GB1562 GB-55%
更多具体细节,请参阅: 降级JSONB列式存储,Hologres助力淘宝搜寻2022双11降本增效!

总结

Hologres的列式JSON计划,真正做到了在放弃JSON易用性和灵活性的同时,兼顾了极致的OLAP查问性能,让用户可能在Hologres上充沛开掘半结构化数据,甚至让Hologres这个一站式实时数仓承当局部数据湖的能力。咱们后续也会持续一直优化列式JSON实现,为大家带来更为极致的性能,敬请期待。

附录

  1. Github数据集: https://www.gharchive.org/
  2. 查问性能测试DDL
CREATE TABLE gh_2015(gh_jsonb jsonb);--开启列式优化ALTER TABLE gh_2015 ALTER COLUMN gh_jsonb SET (enable_columnar_type = on);
  1. 查问性能测试Query
--Query 1SELECT COUNT(1) FROM gh_2015 WHERE gh_jsonb->>'type' = 'WatchEvent';--Query 2SELECT 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 3SELECT 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 closedFROM gh_2015WHERE (gh_jsonb ->> 'type') = 'IssuesEvent'  AND (gh_jsonb -> 'payload' ->> 'action') = 'closed'  AND (gh_jsonb -> 'repo' ->> 'id')::bigint = 41986369GROUP BY 1;--Query 4SELECT event_month,       all_sizeFROM  (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') subWHERE row_num = 1ORDER BY event_month;

理解Hologres:https://www.aliyun.com/product/bigdata/hologram