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

作者:王日宇(霁谦)StarRocks Committer;阿里云技术专家

在电商订单、用户行为和设备日志等场景中,一条数据通常同时包含两类信息:

  • order_idevent_timeamount 等结构相对稳定的业务字段;

  • 商品属性、营销信息、客户端参数等持续变化的扩展字段。

前一类字段适合使用明确的 Schema 进行管理,后一类字段则常以 JSON Payload 的形式保存,以适应不断变化的数据结构。

但当这些 JSON 数据进入湖仓并被频繁查询时,新的问题也随之出现:如果每次过滤、聚合都需要重新解析完整 JSON,并在运行时查找路径、判断类型和完成转换,动态字段越多、访问越频繁,查询成本就越明显。

Variant 为这类数据提供了另一种选择。它将半结构化数据保存为带有类型信息的二进制结构,在保留 Schema 灵活性的同时,减少查询过程中重复解析 JSON 文本的成本。如果少数内部路径逐渐成为长期高频访问的热点,还可以进一步通过 Parquet Shredding,将这些路径保存为类型化子列,使其更好地参与列式读取和计算。

StarRocks 可以通过 Paimon Catalog 读取 Parquet Variant,并使用 get_variant_*variant_queryvariant_typeofCAST 访问其中的动态字段。本文将从普通 Parquet JSON 与 Variant 的区别出发,介绍 StarRocks 如何读取 Paimon Variant、Shredding 如何优化热点路径,以及如何在实际业务中选择合适的数据组织方式。

需要强调的是,Variant 并不是为了替代所有正式类型列。参与分区、Join、排序以及强 SLA 查询的核心字段,仍然更适合建成正式列;如果数据主要用于原文归档,几乎不查询内部结构,普通 Parquet JSON 也可能是更简单的选择。是否使用 Variant,关键不在于"数据是不是 JSON",而在于内部路径是否需要被反复查询、类型是否需要保真,以及热点字段是否值得进一步列式化。

01 Variant 是什么,它和普通 Parquet JSON 有什么区别?

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

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

例如,一条订单事件包含如下 Payload:

复制代码
{
  "sku": "SKU-001",
  "channel": "app",
  "campaign_id": 9001,
  "paid": true,
  "items": [
    {
      "sku": "SKU-001",
      "qty": 2
    }
  ]
}

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

因此,查询 campaign_id 时通常需要经历:

  1. 读取完整 JSON Payload;

  2. 解析 JSON 文本;

  3. 查找目标路径;

  4. 将结果转换成需要的数据类型。

如果多个查询反复访问相同路径,这些解析和类型转换成本也会被反复支付。

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

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

  • metadata:保存字段名称、编码信息等元数据;

  • value:保存值的类型、位置和实际内容。

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

例如:

复制代码
{"campaign_id": 9001}
{"campaign_id": "unknown"}
{"coupon": "NEW20"}

这些结构不同、甚至同一路径类型不同的数据,都可以保存在同一个 Variant 列中。

Apache Parquet 已定义 Variant 的二进制编码和 Shredding布局。

普通 Parquet JSON 与 Parquet Variant 对比

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

Variant 的核心优势可以概括为三点:

  • 保留半结构化数据的 Schema 灵活性;

  • 减少查询时重复解析 JSON 文本的成本;

  • 通过 Shredding,让热点路径具备类型化、列式化的物理表示。

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

02 StarRocks 如何读取 Paimon Variant?

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

整体架构如下:

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

2.1 获取 Paimon 表与 Split 信息

StarRocks 通过 Paimon Catalog 获取表结构和当前 Snapshot,再由 Paimon 完成数据文件的 Split 规划。

StarRocks 抽取 Split 文件信息,使用 StarRocks Parquet Reader 读取 Variant。

2.2 将 Parquet Variant 转换为列式数据

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

  • Plain Variant 中的 metadatavalue

  • Shredded Variant 中的 metadatavaluetyped_value

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

2.3 通过 Variant 函数访问内部字段

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

  • get_variant_string:提取字符串;

  • get_variant_int:提取整数;

  • get_variant_double:提取浮点数;

  • get_variant_bool:提取布尔值;

  • variant_query:返回指定路径下的 Variant;

  • variant_typeof:查看 Variant 值的类型;

  • CAST:将 Variant 转换为目标 SQL 类型。

03 Shredding 如何优化 Variant 读取?

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

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

它已经避免了 JSON 文本语法的重复解析,但所有内部字段仍集中在 Variant 的二进制结构中。

如果业务长期反复访问少数热点路径,例如:

复制代码
$.sku
$.channel
$.campaign_id
$.paid

可以通过 Shredding 将这些路径保存为类型化子列:

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

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

例如,campaign_id 大部分时候是整数,但某一行写入了字符串:

复制代码
{"campaign_id": "unknown"}

整数值可以进入 typed_value.campaign_id,类型冲突的值则继续保存在 Variant 的通用 value 中,保证原始数据语义不会丢失。

Shredding 的读取优势主要来自:

  • 热点路径拥有明确的数据类型,减少运行时类型判断和转换;

  • 热点路径形成独立的 Parquet 类型化子列,可以使用 Parquet 的编码和压缩能力;

  • 查询热点字段时,可以直接利用 typed_value,减少对通用 Variant 内容的解码;

  • 类型化子列为路径级列裁剪和 Parquet 统计信息过滤提供物理基础。

一张 Paimon 表的 Snapshot 会引用多个 Parquet 数据文件。在表结构演进过程中,早期 Parquet 文件中的 payload 可以采用 Plain Variant 物理布局,后续 Parquet 文件中的 payload 可以采用 Shredded Variant 物理布局。

两者都是 Parquet 文件,区别仅在于 Variant 列的物理 Schema。StarRocks Parquet Reader 会根据每个文件的实际 Schema 读取数据,并向查询层提供一致的 Variant 语义。

04 如何使用 StarRocks 查询 Paimon Variant?

下面以电商订单事件表为例,介绍如何使用 StarRocks 查询 Paimon Variant。本文采用阿里云 DLF 托管的 Paimon Catalog 作为示例环境,后续配置与 SQL 均基于该环境展开。

假设 Catalog 中已经存在一张 Paimon 表:

复制代码
demo.order_events

表结构如下:

|-------------------------|---------------|---------------|
| 字段 | 类型 | 说明 |
| order_id | BIGINT | 订单 ID |
| shop_id | BIGINT | 店铺 ID |
| event_time | TIMESTAMP | 事件时间 |
| amount | DECIMAL(18,2) | 订单金额 |
| status | STRING | 订单状态 |
| payload | VARIANT | 商品、营销和客户端扩展信息 |

其中 payload 中的数据可能是:

复制代码
{
  "sku": "SKU-001",
  "channel": "app",
  "campaign_id": 9001,
  "paid": true,
  "items": [
    {
      "sku": "SKU-001",
      "qty": 2
    }
  ]
}

另一行可以包含不同字段:

复制代码
{
  "sku": "SKU-002",
  "channel": "web",
  "paid": false,
  "coupon": "NEW20"
}

4.1 在 StarRocks 中连接 Paimon Catalog

复制代码
CREATE EXTERNAL CATALOG paimon_dlf
PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "rest",
    "uri" = "https://cn-hangzhou-vpc.dlf.aliyuncs.com",
    "paimon.catalog.warehouse" = "<dlf_catalog_name>",
    "token.provider" = "dlf"
);

这里的 paimon.catalog.warehouse 表示 DLF Catalog 名称。uridlf.region 需要替换为 DLF 实例所在地域的实际配置。

创建 Catalog 后即可查看表结构:

复制代码
DESC paimon_dlf.demo.order_events;

结果中可以看到:

复制代码
order_id     BIGINT
shop_id      BIGINT
event_time   DATETIME
amount       DECIMAL(18,2)
status       VARCHAR
payload      VARIANT

4.2 查看完整 Variant

复制代码
SELECT
    order_id,
    payload,
    variant_typeof(payload) AS payload_type
FROM paimon_dlf.demo.order_events
LIMIT 10;

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

4.3 按类型提取字段

复制代码
SELECT
    order_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.demo.order_events;

查询结果示意:

|----------|---------|---------|-------------|-------|
| order_id | sku | channel | campaign_id | paid |
| 10001 | SKU-001 | app | 9001 | TRUE |
| 10002 | SKU-002 | web | NULL | FALSE |
| 10003 | SKU-003 | app | 9001 | TRUE |

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

4.4 读取嵌套对象与数组

复制代码
SELECT
    order_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.demo.order_events;

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

例如:

复制代码
SELECT
    order_id,
    CAST(
        variant_query(payload, '$.campaign_id')
        AS BIGINT
    ) AS campaign_id
FROM paimon_dlf.demo.order_events;

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

复制代码
SELECT
    order_id,
    get_variant_string(payload, '$.sku') AS sku
FROM paimon_dlf.demo.order_events
WHERE get_variant_string(payload, '$.channel') = 'app'
  AND get_variant_bool(payload, '$.paid') = true;

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

复制代码
WITH extracted AS (
    SELECT
        get_variant_string(payload, '$.channel') AS channel,
        get_variant_bool(payload, '$.paid') AS paid
    FROM paimon_dlf.demo.order_events
)
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 的过滤、聚合和分析。

05 如何选择普通 JSON、Plain Variant、Shredded Variant 和正式类型列?

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

在电商订单场景中,一种更合理的建模方式是:

  • order_idshop_idevent_timeamountstatus 使用正式类型列;

  • 商品属性、促销规则、支付回调和渠道扩展信息放入 Variant;

  • skuchannelcampaign_id 等稳定热点路径使用 Shredding;

  • 如果某个动态字段逐渐成为核心过滤、Join 或分区字段,再将它提升为正式类型列。

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

06 常见问题

6.1 Variant 是否一定比普通 JSON 更快?

不一定。Variant 的优势主要来自避免重复解析 JSON 文本,以及为热点路径提供类型化访问。如果查询总是读取完整 Payload、数据只读取一次,或者数据规模很小,普通 JSON 可能更简单。

6.2 Shredding 会把 Variant 变成固定 Schema 的 STRUCT 吗?

不会。Shredding 只把选定热点路径保存为类型化子列;长尾字段和类型冲突值仍可保留在通用 value 中,因此 Variant 的动态结构不会消失。

6.3 同一路径出现不同类型时会怎样?

例如大多数 campaign_id 是整数,但少数行写入字符串 unknown。符合 Shredding 类型的值可以进入 typed_value,类型冲突值继续保存在通用 Variant 内容中。查询时仍应根据业务语义处理 NULL、类型检查和转换失败。

6.4 哪些字段不应该长期留在 Variant 中?

长期参与分区、Join、排序、主键或强 SLA 过滤的字段,应优先提升为正式类型列。Variant 适合承载持续演进的扩展属性,而不是把全部业务 Schema 都隐藏在一个 Payload 中。

07 结论:什么时候值得使用 Paimon Variant?

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

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

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

相关代码已合入 StarRocks Main 分支,将随下一版本正式发布。后续文章也将结合版本进展,进一步介绍不同能力的使用方式与适用场景。

08 参考资料

相关推荐
姜太小白19 小时前
【Oracle】记排查sh脚本调用SQL执行卡顿的排查总结
数据库·sql·oracle
镜舟科技20 小时前
为什么 Text-to-SQL 总是停在 Demo?
数据库·sql·demo·text-to-sql·镜舟科技·mip·语义视图
大牧师1 天前
TypeORM 学习教程
数据库·sql·学习·node.js·orm·nest.js·typeorm
数智启示录1 天前
PostgreSQL 执行计划实战(第 9 篇):SQL 和索引没变,计划为什么突然慢一百倍
数据库·经验分享·sql·postgresql·面试
润乾软件1 天前
SQLazy:从事件表中查出下一组的开始时刻
sql·sqlazy
养生技术人1 天前
Oracle OCP认证考试题目详解082系列第17题
运维·数据库·sql·oracle·ocp
隔窗听雨眠1 天前
活动中台系统慢SQL治理实践:从监控告警到性能优化的全链路方法论
sql·性能优化·github
数智启示录2 天前
PostgreSQL 计划缓存实战(第 10 篇):预编译 SQL 前五次都快,第六次为什么可能变慢
经验分享·sql·缓存·postgresql·面试
ClouGence2 天前
CloudDM 支持达梦、KingbaseES、GoldenDB,国产数据库也能统一管起来
数据库·sql·开源