StarRocks VARIANT 类型实战指南:在 Iceberg 与 Paimon 数据湖上查询半结构化数据
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
VARIANT 是 StarRocks 提供的半结构化数据类型,能够以二进制格式存储整数、浮点数、字符串、布尔值、日期时间等标量类型以及结构体、映射、数组等复杂类型。本文基于 StarRocks v4.1/v4.2 的能力,系统讲解 VARIANT 的查询、取值、类型互转与路径表达式语法,并结合 BE 端源码(be/src/exprs/variant_functions.cpp、be/src/column/variant_path_parser.cpp等)揭示其底层实现原理,帮助读者在数据湖分析场景中灵活驾驭形态多变、schema 频繁演进的半结构化数据。
:::important VARIANT 类型仅支持 Iceberg Catalog 与 Paimon Catalog 中的表,StarRocks 原生表不支持该类型。 :::
从 v4.1 起,StarRocks 支持查询 Parquet 格式 Iceberg 表中的半结构化数据(VARIANT 类型);从 v4.2 起,进一步支持查询 Paimon 表中的 VARIANT 类型数据。本文介绍 VARIANT 的基本概念,以及 StarRocks 如何查询 VARIANT 数据、如何通过 VARIANT 函数进行处理。
VARIANT 是什么
VARIANT 是一种半结构化数据类型,可以存储不同数据类型(schema-less)的值,包括:
- 标量类型:整数(integer)、浮点数(floating-point)、字符串(string)、布尔值(boolean)、日期(date)、时间戳(timestamp);
- 复杂类型:结构体(struct)、映射(map)、数组(array)。
VARIANT 数据以二进制格式编码,以实现高效的存储与查询。它在 Apache Iceberg 表(使用 Parquet 格式并采用 variant encoding 编码,即 Parquet 格式规范中定义的 Variant Encoding 二进制布局)场景下尤其有用:配合灵活的 schema 演进能力,可以高效存储异构数据。Parquet 的 variant encoding 又分为 shredded(打散)与 unshredded(不打散)两种形态,本文后续会结合源码说明 StarRocks 对这两种形态的支持情况。
从源码结构看,StarRocks 在列存储层面以VariantColumn(be/src/column/variant_column.h)承载 VARIANT 数据:内部由_shredded_paths(打散路径)、多个按路径组织的_typed_columns(类型化子列)以及_remain_value_column(未打散的余量值列)协同存储,这为路径查询的高性能批量访问提供了物理基础。
使用 VARIANT 数据
查询 Iceberg 表中的 VARIANT 数据
StarRocks 支持查询以 Parquet 格式存储且使用 variant encoding 编码的 Iceberg 表。查询时,VARIANT 列会被自动识别,无需任何额外 DDL 或注册动作:
-- 查询包含 VARIANT 列的表 SELECT id, variant_col FROM iceberg_catalog.db.table_with_variants;在 BE 端,Parquet 读取器会针对LogicalType::TYPE_VARIANT的列走专门的分支:be/src/formats/parquet/column_reader_factory.cpp中在列类型为 VARIANT 时选择相应的 variant 列读取器,be/src/formats/parquet/group_reader.cpp也会对 VARIANT 类型的 slot 与 schema 节点做特殊处理,从而把 Parquet 中的 variant 编码列完整还原为VariantColumn。
从 VARIANT 数据中提取值
StarRocks 提供了一组函数,用于从 VARIANT 数据中按路径提取强类型的值。这里以get_variant_int、get_variant_string、get_variant_bool、get_variant_double为典型代表(get_variant函数家族,详见函数文档)。
示例 1:使用类型化 getter 函数提取标量值
SELECT get_variant_int(variant_col, '$') AS int_value, get_variant_string(variant_col, '$') AS string_value, get_variant_bool(variant_col, '$') AS bool_value, get_variant_double(variant_col, '$') AS double_value FROM iceberg_catalog.db.table_with_variants;示例 2:使用 JSON path 表达式导航嵌套结构
SELECT get_variant_string(variant_col, '$.user.name') AS user_name, get_variant_int(variant_col, '$.user.age') AS user_age, get_variant_string(variant_col, '$.address.city') AS city FROM iceberg_catalog.db.table_with_variants;示例 3:访问数组元素(下标从 0 开始)
SELECT get_variant_int(variant_col, '$.scores[0]') AS first_score, get_variant_int(variant_col, '$.scores[1]') AS second_score FROM iceberg_catalog.db.table_with_variants;示例 4:查询嵌套 VARIANT 数据并保持 VARIANT 类型返回
SELECT variant_query(variant_col, '$.metadata') AS metadata, variant_query(variant_col, '$.items[0]') AS first_item FROM iceberg_catalog.db.table_with_variants;示例 5:检查 VARIANT 值的类型
SELECT variant_typeof(variant_col) AS root_type, variant_typeof(variant_query(variant_col, '$.data')) AS data_type FROM iceberg_catalog.db.table_with_variants;底层实现:上述函数统一在be/src/exprs/variant_functions.cpp中实现。它们都接受两个参数——VARIANT 列与路径(字符串列),并委托给模板方法_do_variant_query<ResultType>():
get_variant_string对应_do_variant_query<TYPE_VARCHAR>;get_variant_int对应_do_variant_query<TYPE_BIGINT>(为统一所有整数类型,返回值统一为 BIGINT,见be/src/exprs/variant_functions.h中的注释);get_variant_bool对应_do_variant_query<TYPE_BOOLEAN>;get_variant_double对应_do_variant_query<TYPE_DOUBLE>;- 此外还有
get_variant_date、get_variant_datetime、get_variant_time三个日期时间类 getter; variant_query对应_do_variant_query<TYPE_VARIANT>,即路径指向的元素仍以 VARIANT 返回;variant_typeof独立实现,逐行返回 VARIANT 值的类型名称。
执行时,若路径是常量(const path),variant_segments_prepare会在每个 fragment 初始化阶段调用VariantPathParser::parse一次性完成路径解析,并把解析结果VariantPath缓存在FunctionContext中;若路径不是常量,则在每一行重新解析。对于"常量路径 + 类型精确匹配 + 无后缀"的常见形态,代码还提供了一条列级批量快路径(_build_typed_bulk_result/_build_typed_cast_result):直接以 SIMD 批量复制类型化列数据、合并 null 掩码,避免逐行解析开销。对应的单元测试位于be/test/exprs/variant_functions_test.cpp,覆盖了从 JSON 文本构造 VARIANT、类型化 getter、嵌套路径查询等多种场景。
将 JSON 转换为 VARIANT
StarRocks 支持把 JSON 值 CAST 为 VARIANT。如果输入是 STRING,需要先通过parse_json转为 JSON:
SELECT CAST(parse_json('{"id": 1, "flags": {"active": true}, "scores": [1.5, null]}') AS VARIANT) AS variant_value;SELECT CAST(json_col AS VARIANT) AS variant_value FROM db.table_with_json;将 VARIANT 数据转换为 SQL 类型
可以使用 CAST 函数将 VARIANT 数据转换为标准 SQL 类型:
SELECT CAST(variant_query(variant_col, '$.count') AS INT) AS count, CAST(variant_query(variant_col, '$.price') AS DECIMAL(10, 2)) AS price, CAST(variant_query(variant_col, '$.active') AS BOOLEAN) AS is_active, CAST(variant_query(variant_col, '$.name') AS STRING) AS name FROM iceberg_catalog.db.table_with_variants;复杂类型也可以从 VARIANT 转换:
SELECT CAST(variant_col AS STRUCT<id INT, name STRING>) AS user_struct, CAST(variant_col AS MAP<STRING, INT>) AS config_map, CAST(variant_col AS ARRAY<DOUBLE>) AS values_array FROM iceberg_catalog.db.table_with_variants;从实现看,be/src/exprs/cast_expr.cpp针对TYPE_VARIANT提供了完整的双向转换路径(CASE_FROM_JSON_TO(TYPE_VARIANT, ...)、CASE_TO_JSON(TYPE_VARIANT, ...)以及 VARIANT 与普通类型间的cast_to逻辑),因此"JSON → VARIANT → SQL 类型"可以在一条表达式链路里顺畅完成。
将 SQL 类型转换为 VARIANT
可以将 SQL 值 CAST 为 VARIANT。支持的输入类型包括:
- BOOLEAN;
- 整数类型(integer types);
- FLOAT / DOUBLE;
- DECIMAL;
- STRING / CHAR / VARCHAR;
- JSON;
- DATE / DATETIME / TIME;
- 复杂类型:ARRAY、MAP、STRUCT。
注意:MAP 在编码过程中,键会被转换为字符串;HLL、BITMAP、PERCENTILE、VARBINARY 等类型不支持转换。
SELECT CAST(123 AS VARIANT) AS v_int, CAST(3.14 AS VARIANT) AS v_double, CAST(CAST('12.34' AS DECIMAL(10, 2)) AS VARIANT) AS v_decimal, CAST('hello' AS VARIANT) AS v_string, CAST(PARSE_JSON('{"k":1}') AS VARIANT) AS v_json;VARIANT 函数
VARIANT 函数用于查询和提取 VARIANT 列中的数据。三个核心函数如下,每个函数的完整语法、参数与示例详见各自文档:
| 函数 | 作用 | 文档 |
|---|---|---|
variant_query | 按路径表达式查询 VARIANT 值,返回 VARIANT 类型 | variant_query |
get_variant | 从 VARIANT 中按路径提取强类型值(int、bool、double、string) | get_variant |
variant_typeof | 返回 VARIANT 值的类型名称 | variant_typeof |
函数签名速览(来自 get_variant.md):
BIGINT get_variant_int(variant_expr, path) DOUBLE get_variant_double(variant_expr, path) VARCHAR get_variant_string(variant_expr, path) BOOLEAN get_variant_bool(variant_expr, path)返回值语义:若路径对应的元素不存在、路径无效、或值无法转换为目标类型,相关函数返回 NULL。variant_query同理,元素不存在或路径非法时返回 NULL,见 variant_query.md。
VARIANT 路径表达式
VARIANT 函数使用 JSON path 表达式在数据结构中导航,语法与 JSON path 类似:
$表示 VARIANT 值的根;.用于访问对象字段;[index]用于访问数组元素(下标从 0 开始);- 包含特殊字符(如点号)的字段名可以用引号括起来:
$."field.name"。
路径表达式示例:
$ -- 根元素 $.field -- 对象字段访问 $.nested.field -- 嵌套字段访问 $."field.with.dots" -- 带引号的字段名 $[0] -- 第一个数组元素 $.array[1] -- 数组字段的第二个元素 $.users[0].name -- 嵌套数组访问 $.config["key"] -- Map 风格访问源码级语法说明:上述路径由 BE 端的VariantPathParser(be/src/column/variant_path_parser.h)解析,每个路径被拆分为一组VariantSegment,其中kObject表示对象键(key字段),kArray表示数组下标(index字段)。解析器支持:
$根标记(parse_root);.点号键(parse_object_key),未加引号的键仅允许字母数字与下划线([a-zA-Z0-9_]+);[数字]数组下标(parse_array_index);['key']或["key"]带引号键(parse_quoted_key),且支持\n、\t、\r、\"、\'、\\等转义序列;- 路径必须以
$开头,否则解析直接报错(Status::InvalidArgument("Path must start with '$'"))。
从测试用例(be/test/exprs/variant_functions_test.cpp)可以看到,$.commit、$.did、$.time_us等路径会被逐段解析后经VariantPathReader在VariantColumn上定位并读取目标值。
数据类型转换
从采用 variant encoding 的 Parquet 文件读取数据时,支持以下类型转换:
| Parquet Variant 类型 | StarRocks VARIANT 类型 |
|---|---|
| INT8, INT16, INT32, INT64 | int8, int16, int32, int64 |
| FLOAT, DOUBLE | float, double |
| BOOLEAN | boolean |
| STRING | string |
| DATE | Date |
| TIMESTAMP, TIMESTAMP_NTZ | Timestamp |
| DECIMAL | Decimal, float, double |
| STRUCT | Object |
| MAP | Object |
| ARRAY | Array |
可以看到,Parquet 侧的 STRUCT、MAP 在 StarRocks VARIANT 中统一呈现为 Object,而 DECIMAL 可以按需转换为 StarRocks 的 Decimal、float 或 double,这为异构数据源的数据落地提供了较大的灵活性。
限制与注意事项
使用 VARIANT 类型时,请注意以下边界与限制:
- VARIANT 支持从使用 variant encoding(Parquet 格式)的 Iceberg 表读取数据;也支持使用 StarRocks 文件写入器(file writers)写 Parquet 文件,写入采用 unshredded variant encoding。
- VARIANT 同样支持从 Paimon 表读取数据(Paimon 的 VARIANT 列要求使用 Parquet),但仅限由原生读取器(native reader)读取的 split,即仅追加表(append-only tables)与已合并的主键数据(compacted primary-key data)。未合并的主键数据(uncompacted primary-key data)读取需要 JNI reader,目前尚不支持,此类查询会在计划阶段(plan-time)直接报错。
- 单个 VARIANT 值的大小上限为16 MB。
- 目前读与写均仅支持 unshredded variant 值。
- VARIANT 可以由 JSON 值或受支持的 SQL 类型(包括 ARRAY、MAP、STRUCT)CAST 而来。
- 嵌套结构的最大深度取决于底层 Parquet 文件的结构。
需要特别留意的是:即使 StarRocks 当前读写仅支持 unshredded variant,其内存列结构VariantColumn仍内置了 shredded(打散)形态的存储与路径索引能力(_shredded_paths、按路径组织的类型化子列),以便在查询时对常见路径进行列级批量访问优化;这一实现细节可以解释为何"常量路径 + 精确类型匹配"的查询能够走 SIMD 批量快路径。不过在实际使用中,仍应以上述官方限制为准来规划写入链路。
小结
VARIANT 为 StarRocks 的数据湖分析场景补上了"无固定 schema"的一环:对 Iceberg(Parquet variant encoding)与 Paimon 表,你可以直接以 VARIANT 列读取半结构化数据,再通过variant_query、get_variant家族与variant_typeof按 JSON path 提取强类型值,或借助 CAST 在 JSON、VARIANT 与标准 SQL 类型(含 ARRAY/MAP/STRUCT)之间自由转换。结合 BE 端variant_functions.cpp的批量快路径与variant_path_parser的路径解析实现,既保证了查询的易用性,也为高频路径访问保留了列式优化的空间。上手时建议从 Iceberg Catalog 中一个带 variant 编码列的 Parquet 表开始,先用variant_typeof观察根类型,再用get_variant_*逐字段抽取目标值,最后按需 CAST 为分析所需的 SQL 类型。
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考