Apache Iceberg Variant类型:半结构化数据新解法
DataHot 速览
Apache Iceberg v3引入Variant类型,用于在单列中存储任意形状的半结构化数据,并以紧凑二进制形式保持原生类型。相比JSON字符串和固定扁平schema,Variant兼具灵活性与查询性能,且无需预定义schema和迁移。本文是该系列首篇,后续将介绍shredding技术。
为什么值得关注:湖仓从业者需关注Variant如何改善半结构化数据的存储与查询效率,影响表格式设计与引擎兼容策略。
本文目录 9 节
译文
AI 逐段翻译半结构化数据,例如字段随行而异的JSON类文档,始终难以适应围绕固定模式构建的表格式。Iceberg v3为这类数据添加了Variant类型:单个列可以容纳任意、不断演变的形状的值,以紧凑的二进制形式存储,引擎能一致地读写。
这是关于Apache Iceberg中Variant的系列文章的第一篇。它涵盖了Variant是什么、为什么存在,以及它如何融入Iceberg表。下一篇文章将介绍shredding,即将频繁访问的Variant字段存储为类型化、列式数据的技术。Variant存储在Parquet、Avro和ORC中;shredding目前仅适用于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类似,但具有更广泛的原始类型,包括date、timestamp、timestamptz、binary和decimal。它还可以嵌套:
- 一个Variant数组是Variant值的有序集合。与Iceberg列表不同,其元素不受限于单一元素类型。
- 一个Variant对象是字符串键字段的集合,其值本身是Variant值。与Iceberg模式中的结构列不同,其字段不是一组固定的、命名的类型化列。
存储方式🔗
Iceberg没有为Variant定义自己的二进制编码。该类型及其编码来自Apache Parquet项目,Iceberg直接使用该编码,因此variant列映射到带有两个二进制字段的Parquetgroup:
optional group payload (VARIANT(1)) {
required binary metadata;
required binary value;
}
metadata保存值中使用的字段名字典,因此value字节通过整数ID引用每个名称,而不是重复名称字符串。value保存编码数据:标量、数组或对象。数组和对象为每个元素存储一个field_offset(该元素值起始处的字节偏移量),对象还为每个字段存储一个field_id(元数据字典中的索引)。
Variant列本身像任何其他Iceberg列一样通过字段ID寻址,但其metadata和value子字段按名称访问,这对shredding很重要。
跨引擎的读写🔗
- Apache Spark 4.0和4.1创建带有
VARIANT列的表,并读写它们。读取在Spark 4.0上的Iceberg 1.10.0中实现;Iceberg 1.11.0添加了Spark 4.1,并在Spark 4.0和4.1上支持写入shredded Variant。 - Apache Flink 2.1在Iceberg 1.11.0中增加了Variant支持。这仅涵盖未shredded的Variant;shredded写入支持已合并,预计在后续版本中发布。
由于这些引擎都使用相同的Iceberg Variant类型,一个引擎写入的值在另一个引擎中读取时是相同的。
Apache Arrow通过其规范扩展类型在内存中携带相同的Variant值arrow.parquet.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:
| id | event | country | amount |
|---|---|---|---|
| 1 | login | US | null |
| 2 | purchase | UK | 99 |
| 3 | login | null | null |
第3行只携带event,因此country和amount读回为null。Variant不要求每行具有相同结构,因此异构事件可以存在于同一列中。
何时使用Variant🔗
当您无法控制数据形状,或者数据变化速度快于您想要演进模式的速度时,Variant是正确的选择:
- 事件和点击流数据,其中每种事件类型携带不同的字段集,并且新字段随时间出现。
- 应用程序和服务日志,具有结构化但异构的负载。
- IoT和传感器遥测,其中每个设备型号报告自己的读数。
- 第三方API响应和webhook,其模式由他人所有,可能随时更改。
- 稀疏属性,否则会变成宽表,包含大部分为null的列。
Variant不是已知模式的替代品。当字段存在于每一行且您经常查询它时,常规的类型化列或用于固定嵌套形状的结构体更简单、更高效。常见模式是保留稳定、频繁查询的字段作为普通列,并将可变部分放入单个Variant列。
下一步:shredding🔗
到目前为止,Variant列是单个metadata + value对。读取一个字段意味着解码metadata字典,然后如果存在该字段,则从valueblob中反序列化它。这很灵活,但灵活性以性能为代价:由于整个文档驻留在单个value列,引擎无法像处理常规类型列那样,利用每字段的最小/最大统计信息来裁剪数据。碎片化解决了这个问题:它将频繁访问的字段存储为独立的、带类型的Parquet列,因此仅涉及这些字段的查询只读取这些列,并能利用它们的统计信息进行数据跳过,而不适合的字段则回退到无类型值。本系列的下一篇文章将解释碎片化的工作原理及其带来的优势。
参与贡献🔗
Iceberg中的Variant支持仍在发展中,欢迎贡献。 Variant跟踪问题(#10392)跟踪正在进行的工作,您可以通过邮件列表和Slack联系社区。
资源🔗
- Iceberg表规范,Variant类型: 半结构化类型
- Apache Parquet中的Variant: Apache Parquet中的Variant类型用于半结构化数据
- Apache Arrow中的Variant: Parquet Variant规范扩展类型
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏