返回
RSS Apache Iceberg Blog 精选 发布 2026-08-12 08:00 收录于 08-13 51

Apache Iceberg Variant类型:半结构化数据新解法

Apache Iceberg v3引入Variant类型,用于在单列中存储任意形状的半结构化数据,并以紧凑二进制形式保持原生类型。相比JSON字符串和固定扁平schema,Variant兼具灵活性与查询性能,且无需预定义schema和迁移。本文是该系列首篇,后续将介绍shredding技术。
推荐理由:湖仓从业者需关注Variant如何改善半结构化数据的存储与查询效率,影响表格式设计与引擎兼容策略。
湖仓Apache Iceberg

译文 AI 逐段翻译

Apache Iceberg 中的半结构化数据:认识 Variant 类型

半结构化数据(例如各行字段不同的 JSON 类文档)一直难以适应围绕固定模式构建的表格式。Iceberg v3 正是为此类数据添加了 Variant 类型:单个列可以保存任意、不断变化的形状的值,并以引擎一致读写紧凑的二进制形式存储。

这是关于 Apache Iceberg 中 Variant 的系列文章的第一篇。本文涵盖 Variant 是什么、为何存在,以及如何融入 Iceberg 表。下一篇文章将介绍切碎(shredding),即以类型化、列式数据存储频繁访问的 Variant 字段的技术。Variant 以 Parquet、Avro 和 ORC 格式存储;目前仅 Parquet 支持切碎功能。

Variant 解决的问题🔗

考虑形状随时间变化的事件数据:

{"event":"view","page":"/pricing","session":"s-8f21"}{"event":"signup","session":"s-8f21","plan":"pro","price":29.00}{"event":"view","page":"/docs","session":"s-3c07","referrer":{"source":"search","term":"iceberg variant"}}

两种传统方法可以处理这种情况,但各有缺点:

  • 将 JSON 存储为字符串。 这种方式灵活,但读取单个字段意味着解析整个文本。JSON 的类型系统也很薄弱:时间戳只是字符串,数字的精度也不明确。
  • 严格、扁平化的模式。 这种方式查询速度快,但每个新字段都需要模式迁移,而且稀疏或一次性字段会浪费空间。

Variant 与 JSON 一样灵活,但以紧凑、类型化的二进制形式存储数据。值保留其原生类型:时间戳保持为时间戳,小数保持为精确小数,而不会退化为 JSON 的字符串和数字。在值内部,字段名被收集到字典中,并通过 id 引用,因此名称不会在每次出现时都完整写出。无需预先声明模式,因此不同形状的文档可以共存于一个列中,新字段也无需迁移。

Apache Iceberg 中的 Variant🔗

Variant 在 v3 规范中已添加到 Iceberg 类型系统。规范将其归为单独类别:variant “既非原始类型,也非嵌套类型”。它比原始类型更丰富,但又不像结构体或列表那样具有固定的、声明的形状。

Variant 值类似于 JSON,但具有更广泛的原始类型,包括 datetimestamptimestamptzbinarydecimal。它还可以嵌套:

  1. 一个Variant 数组 是 Variant 值的有序集合。与 Iceberg 列表不同,其元素不限于单一元素类型。
  2. 一个Variant 对象 是字符串键字段的集合,其值本身也是 Variant 值。与 Iceberg 模式中的结构体列不同,其字段不是一组固定的、命名的类型化列。

存储方式🔗

Iceberg 并未为 Variant 定义自己的二进制编码。该类型及其编码来自 Apache Parquet 项目,Iceberg 直接使用该编码,因此 variant 列映射到 Parquet ,包含两个二进制字段:

optional group payload (VARIANT(1)) {
  required binary metadata;
  required binary value;
}
  • metadata 保存值中使用的字段名字典,因此 value 字节通过整数 id 引用每个名称,而不是重复名称字符串。
  • value 保存编码数据:标量、数组或对象。数组和对象为每个元素存储 field_offset(该元素值起始的字节偏移量),对象还为每个字段存储 field_id(元数据字典中的索引)。

Variant 列本身像其他 Iceberg 列一样通过字段 ID 寻址,但其 metadatavalue 子字段通过名称访问,这对于切碎功能很重要。

跨引擎读写🔗

  • Apache Spark 4.0 和 4.1 可以创建包含 VARIANT 列的表并进行读写。读取功能在 Iceberg 1.10.0 中 Spark 4.0 上提供;Iceberg 1.11.0 增加了 Spark 4.1 支持,并在 Spark 4.0 和 4.1 上支持写入切碎的 Variant。
  • Apache Flink 2.1 在 Iceberg 1.11.0 中添加了 Variant 支持。这仅涵盖未切碎的 Variant;切碎写入支持已合并,预计在后续版本中提供。

由于这些引擎都使用相同的 Iceberg Variant 类型,一个引擎写入的值在其他引擎中读取时完全相同。

使用 Variant🔗

使用 Spark SQL,您可以将异构事件存储在单个 VARIANT 列中,并使用 variant_get 读回字段。Variant 列需要 Iceberg v3 表:

-- Variant is a v3 type, so the table must be format version 3CREATETABLEevents(idBIGINT,payloadVARIANT)USINGicebergTBLPROPERTIES('format-version'='3');-- Insert events of different shapes into the same columnINSERTINTOeventsVALUES(1,parse_json('{"event": "login", "country": "US"}')),(2,parse_json('{"event": "purchase", "country": "UK", "amount": 99}')),(3,parse_json('{"event": "login"}'));-- Read fields out of the Variant with variant_get(column, path, type)SELECTid,variant_get(payload,'$.event','string')ASevent,variant_get(payload,'$.country','string')AScountry,variant_get(payload,'$.amount','int')ASamountFROMevents;

查询返回每事件一行,字段缺失处为 null

ideventcountryamount
1loginUSnull
2purchaseUK99
3loginnullnull

第 3 行仅包含 event,因此 countryamount 读回为 null。Variant 不要求每行共享相同结构,因此异构事件可以存在于一个列中。

何时使用 Variant🔗

当您无法控制数据形状,或数据变化速度超过您希望演进模式的速度时,Variant 是正确的选择:

  • 事件和点击流数据,每种事件类型携带不同的字段集,并且新字段会随时间出现。
  • 应用程序和服务日志,具有结构化但异构的负载。
  • 物联网和传感器遥测数据,每个设备型号报告自己的读数。
  • 第三方 API 响应和 Webhook,其模式由他人所有,并且可能在没有通知的情况下更改。
  • 稀疏属性,否则会变成大部分为 null 列的宽表。

Variant 不能替代已知模式。当字段出现在每行并且您经常查询时,常规类型化列或用于固定嵌套形状的结构体更简单高效。一种常见模式是将稳定、频繁查询的字段保留为普通列,并将可变部分放入单个 Variant 列。

下一步:切碎🔗

到目前为止,Variant 列是单个 metadata + value 对。读取一个字段意味着解码 metadata 字典,然后从 value blob 中反序列化该字段(如果存在)。这很灵活,但灵活性以性能为代价:由于整个文档驻留在单个 value列中,引擎无法像普通类型列那样利用每字段的最小/最大统计信息来裁剪数据。分片(Shredding)解决了这一问题:它将频繁访问的字段存储为单独的、有类型的Parquet列,因此仅涉及这些字段的查询只需读取这些列,并可以利用其统计信息进行数据跳过,而任何不适合的内容则回退到无类型value。本系列下一篇文章将解释分片的工作原理及其带来的好处。

参与贡献🔗

Iceberg中的Variant支持仍在发展,欢迎贡献。 Variant跟踪问题(#10392) 追踪进行中的工作,您可以通过邮件列表和Slack联系社区。

资源🔗

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存