你团队里大概率有一张这样的表:用户行为埋点、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」的灵活,又不付出「每次全解析」的代价。

三、shredding:它怎么同时做到「灵活 + 可过滤」
光有 typed 二进制还不够。如果引擎查 $.event = 'pay' 还得逐行拆二进制,那跟字符串方案没本质区别。
Variant 的杀手锏是 shredding(拆分) :把那些高频出现、又常被过滤的字段,从二进制里「提升」出来,存成独立的类型化列。
举个例子,如果你的事件流里 event 字段几乎每行都有、而且你经常按它过滤,引擎就把 event 拆成一张独立的 typed 列(带独立的 min/max 统计)。当你写 WHERE variant_get(payload, '$.event', 'string') = 'pay' 时:
- 引擎看到的是被 shred 出来的 typed 列上的谓词;
- 直接走谓词下推 ,用列统计信息剪枝文件(min/max 范围不匹配的文件直接跳过);
- 根本不用打开每一行的 Variant 二进制。
这就是「灵活 + 可过滤」同时成立的原理:灵活的部分留在 Variant 二进制里按需读取,热字段被 shred 成可索引的列供引擎剪枝。 过去「JSON 存字符串 → 每次全解析」的死结,在这里被瓦解了。
两个边界要记牢(来自规范与实测):
- Variant 列不能用作分区键;
- Variant 列不能当表的主键 / 标识符。
所以实务上常见模式是:稳定、高频的字段(如 event_type、user_id、ts)保留为普通 typed 列并做分区,真正多变的部分放进单个 Variant 列。
四、上手:建表与查询(实战 SQL)
Variant 不是嘴上说说,是能直接跑的 DDL/DML。门槛只有一个:表必须是 format version 3。
sql
-- 建表: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,不用等生态追上来。

回到开头那张表。你现在的半结构化数据,落在哪一关?评论区扣个字母:
- A 还是一整列 JSON 字符串,查询全表解析,慢且建不了索引。
- B 已经拍平成几百列,能过滤了,但上游一改 schema 就是一次
ALTER TABLE迁移。 - C 用过 Variant / 类似方案,但被「不能分区、不当主键」的边界卡过。
你最想先把哪张「宽到离谱」的表,改成 Variant?