Apache Iceberg Variant 类型:v3 半结构化数据不再「二选一」

你团队里大概率有一张这样的表:用户行为埋点、IoT 设备上报、或者某个第三方服务的 Webhook 回调。它们的共同点——schema 老变。今天上游加个 coupon_id,明天把 address 从字符串改成嵌套对象。

面对这种半结构化数据,过去你基本只有两条路:

  • 塞进一个 STRING,把 '{"event":"pay","amount":99}' 原样存。查的时候每次全表解析,索引建不上,慢得像在翻一本没目录的字典。
  • 或者,把 JSON 拍平成几百列。能过滤了,但上游每加一个字段,你就得跑一次 ALTER TABLE ADD COLUMN,表越扩越宽,迁移越来越疼。

结果就是那张著名的「800 列宽表」——不是维度建模翻车,是有人把 JSON 拍平了。

Iceberg v3 的 Variant 类型想做一件事:让这两种痛苦同时消失——一列,既灵活,又可被引擎高效过滤。

一、旧困境:半结构化数据只有两条烂路

先把这个两难摆清楚。事件流、日志、IoT 遥测、第三方 API 响应,本质都是「逐行结构可能不同」的数据。你要么牺牲查询性能,要么牺牲灵活性。

老方案 怎么存 代价
JSON 存 STRING 整段文本原样入库 每次查询全表解析,无法谓词下推,建不了索引,宽表里挑字段得正则/JSON 函数硬抠
拍平成宽表 每个 JSON 字段独立成列 schema 一变就 ALTER TABLE 迁移;NULL 泛滥;嵌套结构得手工展平,丢了「schema-on-read」的灵活

两边的痛点恰好互补:字符串方案灵活但查不动,宽表方案查得动但不灵活。过去你只能在「灵活」和「能查」之间二选一。

Iceberg v3 把这一题重新出了——它要的不是二选一,而是在一列里同时拿到两者

二、Variant 是什么:单列装下「逐行异构」的数据

Variant 是 Iceberg v3 规范(format version 3)引入的新列类型,专门侍候半结构化数据。最关键的一点:一个 Variant 列,每一行可以结构都不同

这行是 {event: pay, amount: 99},那行是 {event: login, ip: "1.2.3.4", geo: {...}}——同一列原样容纳,不用提前统一 schema。

但 Variant 不是「把 JSON 当字符串存」那么简单。它的底层是一份紧凑的类型化二进制,复用了 Parquet 的 Variant 编码。也就是说,数据写入时就被解析成 typed 结构:

  • string 就按 string 存,timestamp 保留原生时间戳精度,decimal 保留 decimal——不是全部退化成文本
  • 嵌套、数组、null 都按类型保留,读取时直接拿回结构化结果,而不是再解析一遍字符串。

一句话:Variant 给你「schema-on-read」的灵活,又不付出「每次全解析」的代价。

dilemma

三、shredding:它怎么同时做到「灵活 + 可过滤」

光有 typed 二进制还不够。如果引擎查 $.event = 'pay' 还得逐行拆二进制,那跟字符串方案没本质区别。

Variant 的杀手锏是 shredding(拆分):把那些高频出现、又常被过滤的字段,从二进制里「提升」出来,存成独立的类型化列。

举个例子,如果你的事件流里 event 字段几乎每行都有、而且你经常按它过滤,引擎就把 event 拆成一张独立的 typed 列(带独立的 min/max 统计)。当你写 WHERE variant_get(payload, '$.event', 'string') = 'pay' 时:

  1. 引擎看到的是被 shred 出来的 typed 列上的谓词;
  2. 直接走谓词下推,用列统计信息剪枝文件(min/max 范围不匹配的文件直接跳过);
  3. 根本不用打开每一行的 Variant 二进制。

这就是「灵活 + 可过滤」同时成立的原理:灵活的部分留在 Variant 二进制里按需读取,热字段被 shred 成可索引的列供引擎剪枝。 过去「JSON 存字符串 → 每次全解析」的死结,在这里被瓦解了。

两个边界要记牢(来自规范与实测):

  • Variant 列不能用作分区键
  • Variant 列不能当表的主键 / 标识符

所以实务上常见模式是:稳定、高频的字段(如 event_typeuser_idts)保留为普通 typed 列并做分区,真正多变的部分放进单个 Variant 列。

四、上手:建表与查询(实战 SQL)

Variant 不是嘴上说说,是能直接跑的 DDL/DML。门槛只有一个:表必须是 format version 3

-- 建表:format-version='3' 是前提
CREATE TABLE events (
  id      BIGINT,
  ts      TIMESTAMP,
  payload VARIANT
) USING iceberg
TBLPROPERTIES ('format-version' = '3');

-- 读字段:variant_get(列, '$.路径', '返回类型')
SELECT
  variant_get(payload, '$.event',  'string')  AS event,
  variant_get(payload, '$.amount', 'double')  AS amount
FROM events
WHERE variant_get(payload, '$.event', 'string') = 'pay';

variant_get 是 Iceberg 给 Variant 配的读取函数,第三参指定返回类型(string / double / int / timestamp 等),引擎据此走对应的 typed 列做下推。

设计上的经验法则:

  • 稳定、会被过滤或分区的字段 → 普通列(ts 分区、event_type 过滤);
  • 多变、嵌套、各行列结构不同的部分 → 单个 VARIANT 列兜底。

这样你既拿到了宽表的可过滤性,又免掉了「上游改一次 schema 就 ALTER TABLE 一次」的运维债。

五、生态:谁已经能吃下 Variant

一项新类型值不值得上车,看的是引擎同不同意。Iceberg v3 这一波,主流引擎基本同期到位:

  • Apache Spark 4.0 / 4.1:已支持 Variant 的读写;
  • Apache Flink 2.1:已支持 Variant 读写,意味着流批一体链路能直接落地;
  • Snowflake:Iceberg v3 于 2026-05-07 GA,Variant 在首批支持的 v3 类型之列;
  • Databricks Runtime 18.0+:v3 能力落地,Variant 可用。

这不是 Iceberg 自己在玩,而是「开放表格式 + 主流计算引擎」一起把半结构化数据的存查标准往前推了一步。换句话说,你今天就能在湖仓里用上 Variant,不用等生态追上来。

ecosystem


回到开头那张表。你现在的半结构化数据,落在哪一关?评论区扣个字母:

  • A 还是一整列 JSON 字符串,查询全表解析,慢且建不了索引。
  • B 已经拍平成几百列,能过滤了,但上游一改 schema 就是一次 ALTER TABLE 迁移。
  • C 用过 Variant / 类似方案,但被「不能分区、不当主键」的边界卡过。

你最想先把哪张「宽到离谱」的表,改成 Variant?

posted @ 2026-09-09 23:42  Jackeyzhe  阅读(14)  评论(0)    收藏  举报