Hologres V5.0及以上版本支持通过外部表读取数据湖表中的Variant数据,但内部表暂不支持Variant类型。Variant适用于字段结构频繁变化的半结构化数据,可保留值的原生类型,并通过Parquet Shredding提升常用子字段的查询效率。本文为您介绍Variant的适用范围、操作符、类型转换和查询优化方式。
Variant介绍
事件、日志和设备上报等半结构化数据通常具有字段不固定、同一字段类型可能变化、Schema演进频繁等特点。将此类数据存储为JSON文本便于接入,但查询单个字段时仍需解析完整文本;将字段全部展开为结构化列虽然便于查询,但新增字段需要持续演进表Schema,稀疏字段也会产生大量空值。
Variant用于在保留灵活Schema的同时,提高湖表中半结构化数据的存储和查询效率。Variant采用Apache Parquet Variant编码,以紧凑的二进制格式保存数据,并保留日期、时间戳、带时区时间戳和精确数值等原生类型。单个Variant列可以存储结构不同的对象、数组或标量,新增内部字段不需要修改表Schema。
Hologres支持读取数据湖表中的Variant列,并通过操作符访问嵌套字段、通过typed cast(类型化转换)将字段转换为Hologres原生类型。对于采用Shredding存储的常用字段,查询还可以利用Parquet列裁剪和RowGroup级DataSkip,减少不相关数据的读取。
Variant与JSON/JSONB的区别
JSON、JSONB和Variant均可表示半结构化数据,但适用的数据位置、物理编码和查询优化方式不同。JSON和JSONB的详细说明,请参见JSON和JSONB类型。
|
维度 |
JSON/JSONB |
Variant |
|
适用范围 |
内部表。 |
DLF Paimon等外部表。 |
|
存储格式 |
文本(JSON)、二进制(JSONB)。 |
Parquet Variant编码,支持Shredding。 |
|
写入支持 |
支持 |
不支持,由Spark、Flink等引擎写入Paimon。 |
|
索引支持 |
GIN索引(JSONB)。 |
不支持索引,通过Shredding和RowGroup DataSkip加速。 |
|
操作符 |
|
|
|
类型转换 |
需通过 |
直接typed cast,如 |
|
列裁剪 |
列式JSONB(Hologres V1.3及以上版本)提供有限列裁剪。 |
Shredded子列原生列裁剪。 |
使用限制
-
Variant类型仅支持在Hologres V5.0及以上版本中,通过外部表读取数据湖表(如DLF Paimon表)中的Variant列。
-
Hologres内部表暂不支持Variant类型,不能在内部表中创建Variant类型的列。如需在Hologres内部表中存储半结构化数据,请使用JSON或JSONB类型,详情请参见JSON和JSONB类型。
-
Hologres不支持写入Variant列,仅支持读取。数据湖表中的Variant数据由Spark、Flink等引擎写入Paimon。
-
不支持
#>>操作符(以TEXT形式获取指定路径的值)。如需获取嵌套路径的文本值,可使用#>操作符结合::text类型转换实现。 -
typed cast当前支持
::int8(BIGINT)、::float8(DOUBLE PRECISION)、::bool(BOOLEAN)、::text(TEXT)和::timestamptz(TIMESTAMP WITH TIME ZONE)五种目标类型,不支持::int4(INTEGER)等其他类型的直接转换。如需获取INTEGER值,可先转换为::int8,再在外层进行转换。 -
Variant类型目前不支持GIN索引。
Variant操作符
常用操作符
Variant类型支持的操作符如下表所示,语义与JSON/JSONB操作符保持一致。
|
操作符 |
右操作数类型 |
描述 |
操作示例 |
执行结果 |
|
|
int |
获得Variant数组元素,索引从0开始。 |
|
|
|
|
text |
通过键获得Variant对象域,返回值仍为Variant类型。 |
|
|
|
|
text |
以TEXT形式获得Variant对象域,去除引号。 |
|
|
|
|
text[] |
获取指定路径的Variant对象。 |
|
|
|
|
text |
判断键是否存在于Variant对象中。 |
|
|
Typed Cast(类型化转换)
Variant类型支持通过::type语法将子字段直接转换为原生类型,无需先转为TEXT再解析。支持的目标类型如下表所示。
|
转换语法 |
目标类型 |
描述 |
操作示例 |
执行结果 |
|
|
TEXT |
将Variant值转为文本。对象和数组返回JSON文本,字符串返回带引号的值,数字和布尔值返回其文本表示。 |
|
|
|
|
BIGINT |
将Variant数值转为64位整数。 |
|
|
|
|
DOUBLE PRECISION |
将Variant数值转为双精度浮点数。 |
|
|
|
|
BOOLEAN |
将Variant布尔值转为BOOLEAN。 |
|
|
|
|
TIMESTAMP WITH TIME ZONE |
将Variant的带时区时间值转为TIMESTAMP WITH TIME ZONE。显示结果受当前会话时区影响。 |
|
|
Shredded存储与查询优化
Variant的核心优势在于Shredded存储,即Paimon在写入Variant列时,将高频字段物理拆分为独立的Parquet列(typed_value),查询引擎可以直接读取对应子列,无需解析整个Variant文档。
投影字段裁剪
当查询仅涉及Variant对象中的个别字段时,引擎只读取被Shredded的物理子列,跳过其他字段的数据。例如对一个包含20个字段的Variant列,仅查询v->'age'时,只需读取age对应的typed_value列,I/O开销与读取单个普通INT列相当。
RowGroup DataSkip
对于Shredded子列,Parquet的RowGroup级统计信息(min、max、null_count)自动生效。查询引擎在扫描前根据过滤条件的统计信息跳过不满足条件的RowGroup,大幅减少I/O扫描量。
以下示例对50万行表执行点查,利用Shredded子列的RowGroup统计信息跳过大部分数据。
SELECT id, (v->'seq')::int8 AS seq
FROM ext_dlf.variant_acc.v_skip
WHERE (v->'seq')::int8 = 250000;
通过EXPLAIN ANALYZE可以观察DataSkip效果。
-> Seq Scan on v_skip
Filter: (((v -> 'seq'::text))::bigint = '250000'::bigint)
RowGroupFilter: (((v -> 'seq'::text))::bigint = '250000'::bigint)
[scan_rows=3868]
上述执行计划中,RowGroupFilter表明过滤条件被用于RowGroup级裁剪,全表50万行仅实际扫描3868行。
使用示例
基本读取
以下示例演示如何通过DLF外部数据库读取Paimon表中的Variant列。创建外部数据库的详细语法,请参见CREATE EXTERNAL DATABASE;通过DLF访问Paimon数据的完整流程,请参见基于DLF Catalog访问Paimon数据。
-- 1. 创建外部数据库(如尚未创建)
CREATE EXTERNAL DATABASE ext_dlf WITH
catalog_type 'paimon'
metastore_type 'dlf-rest'
dlf_catalog '<YOUR_CATALOG_NAME>'
comment 'DLF Paimon catalog';
-- 2. 直接读取Variant列
SELECT id, v::text
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY id
LIMIT 10;
执行结果示例如下。
id | v
----+-----------------------------------------
0 | {"age": 21, "city": "Beijing"}
1 | {"age": 27}
2 | {"city": "Beijing", "other": "xxx"}
3 | {"other": "yyy"}
4 | {"age": 28, "nested": {"k": [1, 2, 3]}}
(5 行记录)
多字段共投影
可同时提取Variant对象中的多个字段,每个字段独立进行typed cast。
SELECT
id,
v::text AS raw,
(v->'age')::int8 AS age,
(v->'city')::text AS city
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY id;
执行结果示例如下。
id | raw | age | city
----+-----------------------------------------+------+-----------
0 | {"age": 21, "city": "Beijing"} | 21 | "Beijing"
1 | {"age": 27} | 27 |
2 | {"city": "Beijing", "other": "xxx"} | | "Beijing"
3 | {"other": "yyy"} | |
4 | {"age": 28, "nested": {"k": [1, 2, 3]}} | 28 |
(5 行记录)
当某行中不存在被访问的字段时,对应列返回SQL NULL。
嵌套路径访问
使用#>操作符访问嵌套对象。
SELECT (v#>'{nested,k}')::text AS nested_array
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id = 4;
执行结果如下。
nested_array
--------------
[1, 2, 3]
(1 行记录)
数组下标访问
以Variant值为[10, 20, 30]的行为例,通过数组下标访问各元素。
SELECT
(v->0)::text AS idx0,
(v->1)::text AS idx1,
(v->2)::text AS idx2
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id = 5;
执行结果如下。
idx0 | idx1 | idx2
------+------+------
10 | 20 | 30
(1 行记录)
键存在性检查
使用?操作符判断键是否存在。
SELECT id, v?'age' AS has_age
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id IN (0, 3)
ORDER BY id;
执行结果如下。
id | has_age
----+---------
0 | t
3 | f
(2 行记录)
使用Variant字段排序与聚合
Variant子字段经typed cast后可直接用于排序和聚合。
-- 按age字段排序
SELECT id
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY (v->'age')::int8 NULLS LAST
LIMIT 5;
-- 按age字段聚合
SELECT (v->'age')::int8 AS age, COUNT(*)
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
GROUP BY 1
ORDER BY 1 NULLS LAST;
访问不存在的字段
Variant列的根值可以是任意JSON值类型,不限于对象。对于非对象根值(数组、字符串、数字、布尔值、null),通过->访问不存在的键将返回SQL NULL。以下示例中,id为5至9的行的Variant值依次为[10,20,30]、"scalar string"、12345、true和null。
SELECT
id,
v::text,
(v->'nokey')::int8 AS n_int,
(v->'nokey')::text AS n_text
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id >= 5
ORDER BY id;
执行结果如下。
id | v | n_int | n_text
----+-----------------+-------+--------
5 | [10, 20, 30] | |
6 | "scalar string" | |
7 | 12345 | |
8 | true | |
9 | null | |
(5 行记录)
-
对非对象类型的Variant值访问任何键名,均返回SQL NULL。
-
查询时,Variant类型以JSON文本形式展示。