Doris 2.1.0 Variant 新特性验证
Variant 数据类型
过去 Apache Doris 在应对复杂半结构化数据的存储和分析处理时,一般有两种方式:
-
一种方式是用户提前预定好表结构,加工成宽表,在数据进入前将数据解析好,这种方案的优点是写入性能好,查询也不需要解析,但是使用不够灵活、对表结构发起变更增加运维、研发的成本。
-
使用 Doris 中的 JSON 类型、或是存成 JSON String,将原始 JSON 数据不经过加工直接入库, 查询的时候,用解析函数处理。优点是不需要额外的数据加工、预定义表结构拍平嵌套结构,运维、研发方便,但存在解析性能以及数据读取效率低下的问题。
为了解决上述半结构化数据的挑战,Apache Doris 在2.1 版本中引入全新的数据类型VARIANT
,支持存储半结构化数据、允许存储包含不同数据类型(如整数、字符串、布尔值等)的复杂数据结构,无需在表结构中提前定义具体的列,其存储和查询与传统的 String、JSONB 等行存类型发生了本质的改变,期望其作为半结构化数据首选数据类型,给用户带来更加高效的数据处理机制。
Variant 类型特别适用于处理结构可能随时会发生变化 的复杂嵌套结构。在写入过程中,Variant 类型可以自动根据列的结构和类型推断列信息,并将其合并到现有表的 Schema 中,将 JSON 键及其对应的值灵活存储为动态子列。同时,一个表可以同时包含灵活的 Variant 对象列和预先定义类型的更严格的静态列,从而在数据存储、查询上提供了更大的灵活性。除此之外,Variant 类型能够与 Doris 核心特性融合,利用列式存储、向量化引擎、优化器等技术,为用户带来极高性价比的查询性能及存储性能。
可行性分析
ods_test_log 表 schema 为
+--------------+---------------+------+-------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------+------+-------+---------+-------+
| id | VARCHAR(256) | No | true | NULL | |
| time | BIGINT | No | true | NULL | |
| event | VARCHAR(256) | Yes | true | NULL | |
| properties | TEXT | Yes | false | NULL | NONE |
| dt | DATE | Yes | false | NULL | NONE |
+--------------+---------------+------+-------+---------+-------+
- 根据不同的事件类型,properties 中嵌套的嵌套的kv也是不同的,即符合 Variant 类型功能(随时会发生变化的复杂嵌套结构)
- 采用传统 string 类型存储半结构化数据,随着数据日益增多,现存储占用也日渐增大,Variant 类型优秀的存储性能对此问题具有可行性。
- 需求是查询dwd层的明细表,但针对特殊需求,需要直接查询 ods_test_log 表,时间跨度大点,数据量多时,一个查询通常需要数秒才能出结果,Variant 类型优秀的查询性能对此问题具有可行性。
分析验证
-- 原 ods_test_log 建表
CREATE TABLE ods_test_log (
`id` varchar(256) NOT NULL COMMENT '用户id',
`time` BIGINT NOT NULL COMMENT '时间戳',
`event` varchar(256) COMMENT '事件类型',
`properties` string COMMENT '具备的属性',
`dt` DATE COMMENT '事件时间'
)ENGINE=OLAP
DUPLICATE KEY(`id`, `time`,`event`)
PARTITION BY RANGE (`dt`)
(
FROM ("2024-01-01") TO ("2024-04-07") INTERVAL 1 DAY
)
DISTRIBUTED BY HASH (`event`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"compression"="LZ4",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "1"
);
-- 采用 variant 类型建表
CREATE TABLE ods_test_variant (
`id` varchar(256) NOT NULL COMMENT '用户id',
`time` BIGINT NOT NULL COMMENT '时间戳',
`event` varchar(256) COMMENT '事件类型',
`properties` VARIANT NULL COMMENT '具备的属性',
`dt` DATE COMMENT '事件时间'
)ENGINE=OLAP
DUPLICATE KEY(`id`, `time`,`event`)
PARTITION BY RANGE (`dt`)
(
FROM ("2024-01-01") TO ("2024-04-07") INTERVAL 1 DAY
)
DISTRIBUTED BY HASH (`event`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"compression"="LZ4",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "1"
);
之后导入测试的数据
mysql> select count(1) from ods_test_log;
+----------+
| count(1) |
+----------+
| 8637519 |
+----------+
1 row in set (0.02 sec)
mysql> select count(1) from ods_test_variant;
+----------+
| count(1) |
+----------+
| 8638462 |
+----------+
1 row in set (0.02 sec)
-- 数据量基本一致
存储占用
mysql> show data from ods_test_log ;
+---------------+---------------+----------+--------------+----------+------------+
| TableName | IndexName | Size | ReplicaCount | RowCount | RemoteSize |
+---------------+---------------+----------+--------------+----------+------------+
| ods_test_log | ods_test_log | 1.770 GB | 103 | 8650954 | 0.000 |
| | Total | 1.770 GB | 103 | | 0.000 |
+---------------+---------------+----------+--------------+----------+------------+
2 rows in set (0.00 sec)
mysql> show data from ods_test_variant ;
+-------------------+-------------------+------------+--------------+----------+------------+
| TableName | IndexName | Size | ReplicaCount | RowCount | RemoteSize |
+-------------------+-------------------+------------+--------------+----------+------------+
| ods_test_variant | ods_test_variant | 455.069 MB | 103 | 8648850 | 0.000 |
| | Total | 455.069 MB | 103 | | 0.000 |
+-------------------+-------------------+------------+--------------+----------+------------+
2 rows in set (0.01 sec)
**采用 variant 类型,由优化前的 1.77G (1.77 * 1024MB)下降至 455.1MB ,存储占用下降约 75% **
查询速度
-- variant 查询访问默认是关闭的
-- 查询前设置会话变量
set enable_variant_access_in_original_planner = true;
需求1、各用户的PC和APP访问次数
-- ods_test_log
SELECT
id,
sum(xxx)PC,
sum(xxx)APP
FROM(
SELECT
id,
if(get_json_string(xxx, '$.xx')='xx','xx', 'xx')
FROM
ods_test_log
WHERE dt = xxx
) t
GROUP BY id;
-- ods_test_variant
SELECT
id,
sum(xxx)PC,
sum(xxx)APP
FROM(
SELECT
id,
if(xxx['xxx']='xxx','xx', 'xx')
FROM
ods_test_variant
WHERE dt = xxx
) t
GROUP BY id;
查询速度提升近4倍
需求2、求点击xxxx次数
-- ods_test_log
SELECT
flag,
count(*)
FROM
(
SELECT
get_json_string(properties,'$.xxx')flag
FROM
ods_test_log
WHERE
event = 'xxx'
AND dt = xx)t1
WHERE
flag IN ('xxxx', 'xxxx')
GROUP BY
flag;
-- ods_test_variant
SELECT
flag,
count(*)
FROM
(
SELECT
cast(properties['$xxx'] as text) as flag
FROM
ods_test_variant
WHERE
event = 'xxx'
AND dt = xxx)t1
WHERE
flag in ('xxxx', 'xxxx')
GROUP BY
flag;
注意:如使用过滤和聚合等功能来查询子列, 需要对子列进行额外的 cast 操作(因为存储类型不一定是固定的,需要有一个 SQL 统一的类型)。
**例如 SELECT * FROM tbl where CAST(var[‘titile’] as text) MATCH “hello world” **
查询速度提升48.5倍
需求3、访问用户地区统计
-- ods_test_log
SELECT
DISTINCT id,
get_json_string(xxx,'$.xxx')xx,
get_json_string(xxx,'$.xxx')xx
FROM
ods_test_log
WHERE
dt = xxx
AND get_json_string(properties,'$.xxx') LIKE 'xxxx';
-- ods_test_variant
SELECT
DISTINCT id,
cast(xxx['xxx'] as text) as xx,
cast(xxx['xxx'] as text) as xx
FROM
ods_test_variant
WHERE
dt = xxx
AND cast(properties['xxx'] as text) LIKE 'xxxx';
查询速度提升133倍!
总结
在存储占用方面,采用 Variant 数据类型后,存储占用下降约74%(测试数据较少),与官方宣传测试相符;
在查询速度方面,采用 Variant 数据类型后,查询速度提升倍数不一(测试样例少),但可发现,在需要过滤、聚合、子查询中使用 get_json_string 解析时,Variant 数据类型查询速度提升极其可观;
注意事项:
- 目前 Variant 暂不支持 Aggregate 模型,也不支持将 Variant 类型作为 Unique 或 Duplicate 模型的主键及排序键;
- 推荐使用 RANDOM 模式或者开启 Group Commit 导入,写入性能更高效;
- 日期、Decimal 等非标准 JSON 类型尽可能提取出来作为静态字段,性能更好;
- 二维及其以上的数组以及数组嵌套对象,列存化会被存成 JSONB 编码,性能不如原生数组;
- 查询过滤、聚合需要带 Cast,存储层会根据存储类型和 Cast 目标类型来提示(hint)存储引擎谓词下推,加速查询。