StarRocks 如何查询 Paimon 半结构化数据?Variant、Shredding 与 SQL 实践

作者|霁谦

数据说明:本文示例均为通用化、参数化示例,不包含客户、账号、实例标识或实际业务数据;文中的 ID、路径、地址和值均为占位符。

在订单事件、用户行为、设备日志等场景中,数据通常同时包含两部分:一部分是 order_idevent_timeamount 等结构稳定的字段;另一部分是商品属性、营销信息、客户端参数等持续变化的扩展字段。

稳定字段适合使用明确的数据类型,但扩展字段很难提前定义完整 Schema。把它们保存成 JSON 简单灵活,可一旦需要频繁分析 JSON 内部字段,文本解析和类型转换就会成为持续的查询开销。

Variant 为这类数据提供了新的选择:它保留半结构化数据的灵活性,同时通过类型化二进制表示和 Parquet Shredding,让动态字段也能更好地进入列式分析链路。

一、Variant 是什么?它和普通 Parquet JSON 有什么区别?

本文所说的"普通 Parquet JSON",是指将 JSON Payload 以 STRINGBINARY 或 Parquet JSON Logical Type 的方式保存在 Parquet 中,并不是指 StarRocks 的原生 JSON 类型。

普通 Parquet JSON:灵活,但内部结构对 Parquet 不透明

例如,一条通用事件包含如下 Payload:

json 复制代码
{
  "sku": "<sku>",
  "channel": "<channel>",
  "campaign_id": "<campaign_id>",
  "paid": true,
  "items": [
    {
      "sku": "<sku>",
      "qty": "<quantity>"
    }
  ]
}

当它以普通 JSON 保存到 Parquet 时,底层通常仍由一个 BYTE_ARRAY 承载。Parquet 知道这一列保存了 JSON,但 skuchannelcampaign_id 等内部路径并不是独立的 Parquet 类型列。

因此,查询 campaign_id 时通常需要经历:读取完整 JSON Payload、解析 JSON 文本、查找目标路径,再将结果转换成需要的数据类型。如果多个查询反复访问相同路径,这些解析和类型转换成本也会被反复支付。

Variant:结构灵活,但不再只是文本

Variant 是一种面向半结构化数据的类型化二进制表示。对象、数组和标量在写入时会被编码为:

  • metadata:保存字段名称、编码信息等元数据;
  • value:保存值的类型、位置和实际内容。

逻辑上,不同行仍然可以拥有不同字段;物理上,数据不再是一段只能从头解析的 JSON 文本。例如:

json 复制代码
{"campaign_id": "<numeric_id>"}
{"campaign_id": "unknown"}
{"coupon": "<coupon_code>"}

这些结构不同、甚至同一路径类型不同的数据,都可以保存在同一个 Variant 列中。Apache Parquet 已定义 Variant 的二进制编码Shredding布局。

图 1:普通 JSON 与 Variant 的读取差异(配图可在平台编辑器中补充)

普通 Parquet JSON 与 Parquet Variant 对比

对比维度 普通 Parquet JSON Parquet Variant
物理表示 JSON 文本或二进制 Payload 带类型信息的二进制编码
Schema 灵活性 高,不同行可以有不同结构 高,不同行可以有不同结构
类型保真 查询时经常需要 CAST 编码中保存具体类型信息
路径访问 需要解析 JSON 文本并查找路径 根据二进制结构定位路径
重复查询成本 每次查询都可能重新解析 避免反复解析 JSON 文本语法
内部字段列式化 内部路径不是独立 Parquet 列 可通过 Shredding 将热点路径保存为类型化子列
写入成本 写入相对简单 写入时需要进行 Variant 编码
适用场景 原文归档、内部字段很少查询 Schema 持续变化,同时需要反复分析内部路径

Variant 的核心优势可以概括为三点:保留半结构化数据的 Schema 灵活性;减少查询时重复解析 JSON 文本的成本;通过 Shredding,让热点路径具备类型化、列式化的物理表示。

Variant 并不意味着文件一定更小,也不代表所有查询都会更快。如果数据只写入一次、读取一次,或者查询总是返回完整 Payload,普通 JSON 仍然可能是更简单的选择。

二、StarRocks 如何读取 Paimon Variant?

在 StarRocks 查询 Paimon Variant 的过程中,Paimon 负责管理表的 Snapshot、Schema 和数据文件,StarRocks 负责读取 Parquet 文件并执行 Variant 查询。

图 2:StarRocks 查询 Paimon Variant 的读取链路(配图可在平台编辑器中补充)

整个读取过程可以分为三个阶段。

1. 获取 Paimon 表和 Split 信息

StarRocks 通过 Paimon Catalog 获取表结构和当前 Snapshot,再由 Paimon 完成数据文件的 Split 规划。StarRocks 抽取 Split 文件信息,使用 StarRocks Parquet Reader 读取 Variant。

2. 将 Parquet Variant 转换为列式数据

Parquet Reader 根据文件中的物理 Schema 读取:

  • Plain Variant 中的 metadatavalue
  • Shredded Variant 中的 metadatavaluetyped_value

读取结果被组织为 StarRocks 的 Variant 列向量,继续参与过滤、投影、聚合等向量化计算。

3. 通过 Variant 函数访问内部字段

用户可以使用不同函数访问 Variant:

  • get_variant_string:提取字符串;
  • get_variant_int:提取整数;
  • get_variant_double:提取浮点数;
  • get_variant_bool:提取布尔值;
  • variant_query:返回指定路径下的 Variant;
  • variant_typeof:查看 Variant 值的类型;
  • CAST:将 Variant 转换为目标 SQL 类型。

三、Shredding 如何优化 Variant 读取?

Plain Variant 的物理结构可以简化为:

text 复制代码
payload
├── metadata
└── value

它已经避免了 JSON 文本语法的重复解析,但所有内部字段仍集中在 Variant 的二进制结构中。如果长期反复访问少数热点路径,例如 $.sku$.channel$.campaign_id$.paid,可以通过 Shredding 将这些路径保存为类型化子列:

text 复制代码
payload
├── metadata
├── value
└── typed_value
    ├── sku             STRING
    ├── channel         STRING
    ├── campaign_id     BIGINT
    └── paid            BOOLEAN

图 3:Plain Variant 与 Shredded Variant 的物理布局(配图可在平台编辑器中补充)

Shredding 并没有把 Variant 变成固定 Schema 的 STRUCT。没有被 Shredding 的长尾字段仍然保存在 value 中,类型不符合预期的数据也可以通过 value 保留。

例如,campaign_id 大部分时候是整数,但某一行写入了字符串 unknown。整数值可以进入 typed_value.campaign_id,类型冲突的值则继续保存在 Variant 的通用 value 中,保证原始数据语义不会丢失。

Shredding 的读取优势主要来自:

  • 热点路径拥有明确的数据类型,减少运行时类型判断和转换;
  • 热点路径形成独立的 Parquet 类型化子列,可以使用 Parquet 的编码和压缩能力;
  • 查询热点字段时,可以直接利用 typed_value,减少对通用 Variant 内容的解码;
  • 类型化子列为路径级列裁剪和 Parquet 统计信息过滤提供物理基础。

一张 Paimon 表的 Snapshot 会引用多个 Parquet 数据文件。在表结构演进过程中,早期文件中的 payload 可以采用 Plain Variant 物理布局,后续文件可以采用 Shredded Variant 物理布局。两者都是 Parquet 文件,区别仅在于 Variant 列的物理 Schema。StarRocks Parquet Reader 会根据每个文件的实际 Schema 读取数据,并向查询层提供一致的 Variant 语义。

四、使用 StarRocks 查询 Paimon Variant

下面以通用事件表为例。假设 DLF REST Catalog 中已经存在一张 Paimon 表:

text 复制代码
<database_name>.<table_name>

表结构如下:

字段 类型 说明
event_id BIGINT 事件 ID
event_time TIMESTAMP 事件时间
amount DECIMAL(18,2) 示例数值字段
status STRING 状态字段
payload VARIANT 动态扩展信息

1. 创建 Paimon DLF REST Catalog

sql 复制代码
CREATE EXTERNAL CATALOG paimon_dlf
PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "rest",
    "uri" = "<dlf_rest_endpoint>",
    "paimon.catalog.warehouse" = "<dlf_catalog_name>",
    "token.provider" = "dlf"
);

paimon.catalog.warehouse 表示 DLF Catalog 名称,服务地址和地域参数需要替换为对应环境的配置。创建 Catalog 后即可查看表结构:

sql 复制代码
DESC paimon_dlf.<database_name>.<table_name>;

2. 查看完整 Variant

sql 复制代码
SELECT
    event_id,
    payload,
    variant_typeof(payload) AS payload_type
FROM paimon_dlf.<database_name>.<table_name>
LIMIT <row_limit>;

对于示例中的对象数据,payload_type 通常为 Object

3. 按类型提取字段

sql 复制代码
SELECT
    event_id,
    get_variant_string(payload, '$.sku') AS sku,
    get_variant_string(payload, '$.channel') AS channel,
    get_variant_int(payload, '$.campaign_id') AS campaign_id,
    get_variant_bool(payload, '$.paid') AS paid
FROM paimon_dlf.<database_name>.<table_name>;

如果路径不存在,或者实际值无法转换为目标类型,对应的 get_variant_* 函数返回 NULL

4. 读取嵌套对象和数组

sql 复制代码
SELECT
    event_id,
    variant_query(payload, '$.items[0]') AS first_item,
    get_variant_string(payload, '$.items[0].sku') AS first_item_sku,
    get_variant_int(payload, '$.items[0].qty') AS first_item_qty
FROM paimon_dlf.<database_name>.<table_name>;

variant_query 返回的仍然是 Variant,适合继续访问嵌套结构;如果需要明确的 SQL 类型,可以使用 get_variant_*CAST

sql 复制代码
SELECT
    event_id,
    CAST(variant_query(payload, '$.campaign_id') AS BIGINT) AS campaign_id
FROM paimon_dlf.<database_name>.<table_name>;

5. 使用 Variant 字段进行过滤和聚合

sql 复制代码
SELECT
    event_id,
    get_variant_string(payload, '$.sku') AS sku
FROM paimon_dlf.<database_name>.<table_name>
WHERE get_variant_string(payload, '$.channel') = '<channel>'
  AND get_variant_bool(payload, '$.paid') = true;

也可以先抽取动态字段,再参与聚合:

sql 复制代码
WITH extracted AS (
    SELECT
        get_variant_string(payload, '$.channel') AS channel,
        get_variant_bool(payload, '$.paid') AS paid
    FROM paimon_dlf.<database_name>.<table_name>
)
SELECT
    channel,
    COUNT(*) AS event_count,
    SUM(CASE WHEN paid THEN 1 ELSE 0 END) AS paid_count
FROM extracted
GROUP BY channel;

这样,Paimon 表中的动态 Payload 就可以像普通类型列一样参与 StarRocks 的过滤、聚合和分析。

五、如何选择 JSON、Variant 和正式类型列?

图 4:半结构化数据建模选择(配图可在平台编辑器中补充)

数据特征 推荐方式
需要保留 JSON 原文,几乎不查询内部字段 普通 Parquet JSON
Schema 经常变化,需要反复访问内部路径 Plain Variant
文档较宽,长期反复访问少数热点路径 Shredded Variant
字段参与分区、Join、排序或强 SLA 查询 正式类型列

一种更合理的建模方式是:

  • 稳定 ID、事件时间、数值和状态字段使用正式类型列;
  • 变化频繁的属性和扩展信息放入 Variant;
  • 稳定热点路径使用 Shredding;
  • 如果某个动态字段逐渐成为核心过滤、Join 或分区字段,再将它提升为正式类型列。

Variant 的价值不是把所有字段都塞进一个 Payload,而是在"固定 Schema"和"完全动态 JSON"之间提供更合适的平衡。

总结

普通 Parquet JSON 解决了动态数据的存储问题,但频繁分析内部字段时,需要持续承担文本解析和类型转换成本。Variant 将半结构化数据编码为带类型信息的二进制结构,在保留 Schema 灵活性的同时,让动态字段重新进入列式计算体系。

在此基础上,Shredding 可以进一步将热点路径保存为类型化 Parquet 子列,减少通用 Variant 解码和类型转换,并为列裁剪、编码压缩和统计过滤提供物理基础。

通过 Paimon DLF REST Catalog,StarRocks 用户可以直接使用 get_variant_*variant_queryvariant_typeofCAST 查询 Paimon Variant 数据,将动态 Payload 与普通结构化字段放在同一套 SQL 分析链路中。

相关推荐
这个DBA有点耶1 小时前
MySQL迁移实战:从mysqldump到专业工具的完整选型指南
数据库·mysql·dba
小白说大模型2 小时前
LLM集成数据库的幻觉治理:当AI给出的SQL建议是错的
数据库·人工智能·sql·oracle·重构·开源
zcmodeltech2 小时前
工程机械与矿山机械沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的露天开采-井下掘进-智慧矿山全场景联动方案
数据库·stm32·单片机·嵌入式硬件·制造·多分类
曹牧2 小时前
Oracle:空值排序
数据库·oracle
SelectDB3 小时前
Apache Doris 与 StarRocks 深度对比:2026 年 OLAP 引擎选型指南
数据库
lbb 小魔仙3 小时前
谁替 AI Agent 记住时间?——从航海钟到智能数据库,三百年时延坍缩史
数据库·人工智能·db
这个DBA有点耶3 小时前
读懂半连接:IN/EXISTS子查询什么时候快、什么时候慢?
数据库·性能优化·架构
01_ice3 小时前
MySQL内外连接
数据库·mysql
ClouGence4 小时前
2026 年 4 款数据库管理工具推荐:免费、开源、付费怎么选?
数据库·后端·开源