Apache Iceberg Variant 与 Shredding:机制、上游进展与实测归因

从 Variant 二进制编码到 shredding 列裁剪 —— 哪些性能收益已经兑现,哪些还卡在上游,以及三组 40 亿行级实测数据的正确归因

最后更新 2026-07-29  ·  上游状态截至同日  ·  基于 Spark 4.1.2 / 4.2.0 + Iceberg 1.11.0 + Amazon S3 Tables 实测

0. 结论速览

Variant 当前处于「功能可用、存储已兑现、Schema 自描述已兑现,但 shredding 查询加速尚未兑现」的阶段。

如果你只带走三句话:

  • Variant 有两种完全不同的性能收益,必须分开谈。「二进制编码免去 JSON 文本解析」现在就能拿到;「shredding 列裁剪 / 谓词下推」在任何已发布版本中都还拿不到。混淆这两者是几乎所有 Variant 性能报告的通病。
  • 我们自己三组实测里那些漂亮的提升数字,来源是前者,不是 shredding。其中一组(Agent Trace Log)的 shredding 配置键根本写错了,表大概率从未启用 shredding —— 详见 §5
  • 完整收益需要 Iceberg 侧合并 PR #16714/#16715 + Spark 4.3 两个条件同时满足,且相关 PR 已多次被 stale bot 关闭,排期上不宜乐观 —— 详见 §6
能力维度成熟度状态
功能可用性(建表 / 读写 / 嵌套 / 数组 / 类型) 已就绪 Spark 4.1 + Iceberg 1.11 + S3 Tables 全链路打通,可生产使用
存储效率 已兑现 相同数据比 JSON String 省 13.8% ~ 16%(大文件),纯二进制编码单独可达 31.4%
运行时 Schema 自描述 已兑现 schema_of_variant_agg 无需采样即可得到精确字段名与类型
查询性能 —— 二进制编码免解析 已兑现 长 JSON / 深路径 / 大表扫描场景快 1.16x ~ 1.90x;短 JSON 场景反而更慢
查询性能 —— shredding 列裁剪 尚未兑现 Iceberg Spark connector 未实现下推接口,单列查询仍需重建整个 Variant
查询性能 —— file / row-group skipping 尚未兑现 最薄弱环节:SPARK-55817 仍 Open,对应 PR 已被 stale bot 关闭

1. 设计初衷与目标

先把「Variant 到底为什么存在」讲清楚,后面所有性能讨论都建立在这个基础上。

1.1 解决的是「JSON 字符串重复解析」这个老问题

Variant 不是为某个新潮场景发明的。各社区的原始 proposal 用几乎一致的语言描述动机:

一句话概括二进制编码取代 JSON 文本,在不要求用户预定义 schema 的前提下消除逐行重复解析的开销 —— 即「既要 JSON 的灵活,又要列存的性能」。

1.2 两层目标,正好对应「存储已兑现 / 查询未兑现」的分界

公开 spec 把目标拆成了两层,这个拆分是理解后文一切的钥匙:

规范目标
① Variant Binary Encoding VariantEncoding.md 让半结构化数据 "can be efficiently queried by path";嵌套值自包含("each nested Variant value is contiguous and self-contained"),便于在宽 / 深结构中高效访问
② Variant Shredding VariantShredding.md 出发点是 "data is often partially homogeneous",故把高频字段抽取成独立 Parquet 列,解锁三件事:
(a) 更紧凑的列式编码
(b) 用列统计做 data skipping
(c) partial projections(部分投影)
关键洞察 我们实测中「读取慢、列裁剪不生效」恰恰是 shredding spec 第 (b)(c) 两条目标尚未在 Iceberg connector 落地的直接表现。设计目标是清晰的,缺的只是实现。

1.3 附带能力:运行时 Schema 自描述

Spark 提供公开内置函数 schema_of_variant / schema_of_variant_agg,可在查询运行时从数据本身推断出 Variant 的结构和类型,无需事先定义或维护 schema。这是 §1.1 中 "schemaless / 保留源数据灵活性" 设计目标的自然结果 —— schema 信息内嵌在数据里、可被运行时读出。

相比之下,JSON String 列对引擎是不透明的:DESCRIBE table 只能看到 payload STRING,任何消费方想知道里面有什么字段都得自己采样猜。这条能力与 shredding 无关,当下即可使用,实测见 §8


2. 技术机制

2.1 为什么传统两种方案都不行

方案问题
VARCHAR 字符串查询时全列反序列化,无谓词下推,无 row group 跳过
展开为宽表Schema 膨胀(50 字段 = 50 列),大量 nullable 列;结构变化需 ALTER TABLE;不适合逐行结构差异大的数据

2.2 物理存储格式

VARIANT 在 Parquet 文件中存储为带两个 binary 字段的 group:

optional group variant_col (Variant(1)) {
  required binary metadata;   -- 类型信息(字段名字典、类型编码)
  required binary value;      -- 实际数据
}

metadata 携带结构信息(字段名到 ID 的映射),value 存储编码后的值。这种自描述设计使每行可以有不同结构,同时支持列级读取。

VARIANT 的类型范围比 JSON 更宽:除 JSON 原生的 string / number / boolean / null / array / object 之外,还原生支持 date、timestamp(含 / 不含时区)、decimal、binary。存为 date 的字段查询时不需要运行期转换,范围过滤直接生效。

2.3 Shredding 做了什么

写入时:引擎分析被写入的 VARIANT 值,将高频出现的字段额外提取为独立的 typed Parquet 列(typed_value)。主 VARIANT 二进制列完整保留 —— 无信息丢失、不施加 schema 约束,shredded 列是额外的「快速通道」。

读取时(理论上):查询访问被 shredded 的字段时直接读 typed 列,跳过二进制解码;谓词可下推到 typed 列,实现 row group / page 级跳过。

现实检查 上一段「读取时」是 spec 的承诺,不是当前的行为。截至 2026-07-29,在 Spark 4.1/4.2 + Iceberg 1.11.0 上,variant_get() 仍是纯运行时函数:读完整 variant binary → 解析元数据 → 定位字段 → 类型转换。我们的 EXPLAIN FORMATTED 直接证实了这一点(见 §4.1)。

2.4 Manifest 中的 shredded subcolumn 统计(V3 新增)

为支持 shredded subcolumn 的 file pruning,V3 扩展了 manifest 中 VARIANT 列的 bounds 编码方式:

列类型lower_bounds / upper_bounds 编码
普通列Map<columnId, binary>,binary 是 primitive 值的序列化
VARIANT 列(V3)binary 改为二进制序列化的 Variant object{ "$.event_ts": <bound>, "$.location.longitude": <bound> },key 为 JSONPath 格式的 subcolumn path

对现有 manifest 格式完全兼容(value 仍是 binary)。Bounds 收集条件:该文件内 subcolumn 所有值类型一致才收集;混合类型或全 null 不收集。value_counts / null_value_counts 等统计推迟到 Iceberg format V4

2.5 读写函数

函数行为
parse_json(str)解析 JSON 字符串,保留 JSON 原始类型(字符串化的日期自动转换)。Kafka / webhook / 应用日志直接落地用这个
to_variant(value)将 SQL 类型值转为 VARIANT,保留原类型(DATE → date,不变为字符串)。从已有 SQL 类型数据用这个
variant_get(col, '$.path', 'TYPE')提取字段并返回指定 SQL 类型,无需额外 CAST
try_variant_get(...)同上但类型不匹配返回 null 而非报错 —— 脏数据场景必备
schema_of_variant_agg(col)聚合推断整列的 schema —— 无需采样即可知道有哪些字段及其类型
dot notation 不可用 detail.map_id 这种写法在 Spark 4.1.2 上直接报错 INVALID_EXTRACT_BASE_FIELD_TYPENeed a complex type [STRUCT, ARRAY, MAP] but got "VARIANT")。必须用 variant_get()

3. 四层性能来源辨析(本文核心)

市面上几乎所有 Variant 性能讨论 —— 包括我们自己早期的测试报告 —— 都把下面四件事混为一谈,导致归因错误。它们的实现状态完全不同,必须分开看。

#机制省什么依赖 shredding?当前状态
二进制编码免 JSON 文本解析
binary offset 直接定位 vs 文本线性扫描
CPU 不依赖 已兑现
存储节省
消除 JSON 语法字符 + 列式字典编码
存储 / IO 部分依赖 已兑现
列级读取下推 / 投影裁剪
只读请求路径的 typed_value 列,不重建整个 Variant
少读 强依赖 未兑现
file / row-group skipping
shredded 子列 min/max 统计 → 跳过整个文件或 row group
少读文件 强依赖 未兑现(最薄弱)
为什么这个区分至关重要

① 和 ② 与 shredding 无关,是 Variant 格式本身的收益,今天就能拿到

③ 和 ④ 才是 Variant 的「杀手级」卖点 —— 把半结构化字段当成真正的列来裁剪、下推、跳过 —— 而它们在当前任何已发布版本中都拿不到

后果:一份报告如果测出 Variant 快了,然后归因给 "Shredding 生效",那么它几乎肯定归因错了 —— 快的是 ①,而不是 ③。这正是我们自己踩过的坑(§4.3)。

还有一条容易和上面四条混淆的独立线:向量化读取(Arrow VarBinary 批量读 metadata + value)。它已经合并进 Iceberg 1.12.0,但只服务非 shredded 表,对 shredded 表会主动关闭批量读退回逐行。详见 §6.2


4. 我们的实测数据

三组测试呈现了与工作负载强相关的不同表现。单看任何一组都会得出错误结论,必须合起来读。

测试数据量JSON 特征结论shredding 实际状态
游戏行为日志
2026-06-01
42 亿行
342 GB
~150 B,~6 字段,1 层 Variant 慢
0.31x ~ 0.78x
已启用
表属性正确
COVID 多源数据
2026-05
2 亿行 多源异构 Variant 快
16% ~ 43%
未使用列裁剪
Agent Trace Log
2026-06-15
4.11 亿行
20 GB
500–2000 B,20+ 字段,2–3 层 Variant 快
1.38x ~ 1.90x
大概率未启用
配置键写错

4.1 游戏行为日志:42 亿行,Variant 全面落后

环境:4,206,291,624 行 · bucket(8, aid) 分桶 · S3 Tables Iceberg V3 zstd · Spark 4.1.2 + Iceberg 1.11.0 · 1 次 warmup + 3 次正式取均值

存储

分桶策略JSONVariant平均文件大小Variant 节省
无分桶344.6 GB294.4 GB54 / 124 MB14.6%
bucket(8, aid)342.4 GB293.1 GB~55 MB14.4%
bucket(64, aid)251.2 GB249.6 GB~5.4 MB0.65%
存储节省与文件大小强相关 平均文件 50–130 MB 时节省稳定在 14–15%;降到 5.4 MB 时几乎归零(0.65%)。原因:小文件下每列只有约 8.8 万行(42 亿 / 47,424 文件),字典编码复用率低,而 Parquet 元数据开销占比升到 0.5–1%,正好抵消列式收益。大量分桶 + Variant 是反模式。

查询性能

Query场景JSON (ms)Variant (ms)比率
Q1单玩家 + map_id 过滤(点查 + 嵌套谓词)28,54064,7600.44x
Q2时间范围 + map_type 过滤(分区裁剪)7132,3150.31x
Q3单玩家按 map_id 聚合(3 次 variant_get)15,43442,4930.36x
Q4多字段提取(5 个嵌套字段)24,64558,3520.42x
Q5嵌套条件聚合15,39041,6270.37x
Q61 天数据大范围聚合11,78215,1470.78x

单函数对照更干净:variant_get(detail, '$.map_id', 'STRING') = 40,120 ms vs get_json_object(detail, '$.map_id') = 27,844 ms

根因:EXPLAIN 直接证伪了列裁剪

EXPLAIN FORMATTED 显示 Output: [aid, detail] —— 即使字段已被 shredded 成独立 Parquet 列(该表 shredding 后 Parquet 从 3 列增至约 990 列),variant_get 仍读取完整 detail 列,无任何列裁剪。

诊断链条:

  • Parquet 存储层:shredded 列确实写入了
  • Spark 4.1 已定义 SupportsPushDownVariantExtractions 接口
  • Iceberg 1.11.0 的 SparkScanBuilder 未实现该接口 ← gap 所在
  • 结果:variant_get() 退化为纯运行时函数

Q6(0.78x)差距最小,因为大范围扫描下 IO 开始成为瓶颈,Variant 少读 14% 数据的存储优势部分抵消了 CPU 劣势。

社区独立印证 2026-06-04 Apache Iceberg Variant 社区同步中,Shagang(Sang) 用 GitHub activity 数据测试:shredding 后裂解出约 300 列,但查询只投影单列时仍需从 300 个裂解列重建整个 Variant,性能「明显差于」纯 JSON。Steve Loughran 的基准显示 implicit shredding 下读取慢近 5x两条独立路径得出同一诊断。

4.2 COVID 多源数据:2 亿行,Variant 反超

规模Variant vs JSON String备注
~100 万行基本持平(1.08x)字段少时两者接近
~2 亿行Variant 快 16%二进制编码免去逐行 JSON 文本解析
Q6 全表聚合(covidcast 2 亿行)Variant 快 20%16 executor 下进一步放大到快 41%~43%

存储侧:2 亿行物理实测节省 16%(15.3 vs 17.7 bytes/row)。此优势与 Catalog 后端无关 —— S3 Tables 与 Glue 趋势一致。

这组数据的重要性在于:它是「机制 ①」的第一个干净样本。大表全表扫描时 CPU 不再被逐行 variant_get 调用主导,二进制编码免解析的优势显现。

4.3 Agent Trace Log:4.11 亿行,Variant 大胜 —— 但原因不是 shredding

环境:411,141,272 行 OTel spans(Kafka Structured Streaming 实时写入)· S3 Tables Iceberg V3 zstd · Spark 4.1.2 on EKS(10 executors × 4 cores, 12 GB)· 分区 days(start_time)

结果

Query场景Variant (ms)JSON (ms)Speedup胜出
Q1按 service 聚合 token 消耗29,47941,2591.40xVariant
Q2全表聚合(所有 service 的 token 统计)34,45165,4171.90xVariant
Q3按 attribute 值过滤计数26,77536,9771.38xVariant
Q4慢 span Top-100(LIMIT)3591800.50xJSON
Q5trace_id 单点查询(调用链重建)55,07436,1320.66xJSON

存储:Variant 20.39 GB(381 文件 / 平均 54.80 MB)vs JSON 23.65 GB(76 文件 / 平均 318.60 MB),节省 13.8%(3.26 GB)。

归因修正:这不是 shredding 的功劳,而且这张表大概率没开 shredding

原报告把 1.38x~1.90x 归因给 "Shredding 将高频 key 物化为独立列,聚合时直接列式读取"。这个归因是错的,有四条独立证据:

证据 1:配置键在 Spark 中不存在

报告的测试环境写的是:

spark.sql.parquet.variantShreddingEnabled: "true"

核对 Spark master 的 SQLConf.scala,所有 variant 相关配置都在 spark.sql.variant.* 下,没有 spark.sql.parquet.variantShreddingEnabled 这一项:

真实配置键默认值作用
spark.sql.variant.writeShredding.enabledtrue允许 Parquet writer 写 shredded variant
spark.sql.variant.allowReadingShreddedtrue允许 reader 读 shredded variant
spark.sql.variant.pushVariantIntoScantruescan schema 中用 struct 替换 variant
spark.sql.variant.shredding.maxSchemaWidth300推断时最多 shred 多少字段
spark.sql.variant.inferShreddingSchema推断 shredding schema

Spark 对未知的 spark.sql.* 配置静默接受、不报错,所以这一行等于什么都没做,而且不会有任何警告。

证据 2:即使键名写对了,Iceberg 也不看它

spark.sql.variant.*Spark 内置 Parquet data source 的开关,而我们写的是 Iceberg 表。Iceberg 有自己独立的开关:

// Iceberg 1.11.0 core/src/main/java/org/apache/iceberg/TableProperties.java:161
public static final String  PARQUET_SHRED_VARIANTS         = "write.parquet.shred-variants";
public static final boolean PARQUET_SHRED_VARIANTS_DEFAULT = false;   // ← 默认 false

Iceberg 的 Parquet.WriteBuilder.forTable(table) 走的是 setAll(table.properties()) —— 只读表属性variantShreddingFunc 由此装配。Spark session 里的 spark.sql.variant.* 完全不参与 Iceberg 的写入决策。

默认 false + 报告未设置该表属性 ⇒ 该表极可能是未 shredded 的纯 binary Variant 表。

证据 3:对照组设对了

同期的游戏数据测试报告明确记录了表属性:

Table Properties: [format-version=3, write.parquet.shred-variants=true,
                   write.parquet.compression-codec=zstd]

这也解释了为什么游戏报告能观察到「shredding 后 Parquet 从 3 列增至约 990 列」这类物理证据,而 Agent Trace 报告通篇没有任何 shredded 列数、EXPLAIN 输出或 Parquet schema 的直接证据 —— 所有「Shredding 生效」的表述都是推断,不是观测。

证据 4:报告自身前后矛盾

报告位置说法问题
§Q1 分析"Shredding 将高频 key 物化为独立列,聚合时直接列式读取"与两周前同栈游戏测试的 EXPLAIN FORMATTED 直接冲突
§Q3 分析"WHERE 条件中直接使用 Shredding 列进行过滤"同上;SupportsPushDownVariantExtractions 在 Iceberg 1.11.0 中未实现
§未来预期"当社区完成 SupportsPushDownVariantExtractionsvariant_get() 将被优化为只读取 shredded 列"这句自我否认了前两条

报告同时声称「列裁剪已生效」和「列裁剪尚未实现」,二者不能同时为真。同一套技术栈在两周前已被 EXPLAIN 证伪,所以正确的一半是「尚未实现」

那 1.38x~1.90x 到底来自哪里?

来自机制 ① —— Variant 二进制编码免去 JSON 文本解析。有意思的是,报告自己的根因分析其实说对了机制,只是错误地挂在了「Shredding」名下:

因素游戏 detailAgent Trace attributes
JSON 字符串长度~150 bytes500–2000 bytes
字段数~620+
路径深度1 层($.gold2–3 层$.gen_ai.usage.input_tokens

连带修正:Q5 的 0.66x 也归因错了

报告把点查慢归因为「Shredding 后 Parquet 列数更多、I/O 量更大」。若真未 shredded,这解释站不住。更可能是文件布局差异

Variant 表 381 文件 × 55 MB vs JSON 表 76 文件 × 319 MB。全表扫描找几行时,文件数多 5 倍意味着 5 倍的 footer 读取和任务调度开销。报告自己也注明了「Streaming 触发频率相同但 Spark 合并策略不同」导致文件数差异,却未把它纳入性能归因

Q4 的 0.50x 绝对值只有 359 ms vs 180 ms,可忽略。

修正后的结论表

报告原结论修正
"Shredding 对聚合查询效果显著(Q2 近 2x)" 应为「Variant 二进制编码对聚合查询效果显著」。Shredding 大概率未启用;即便启用,其列裁剪在 Iceberg 1.11.0 也不生效
"WHERE 过滤 ~1.4x:Shredding 列支持谓词下推" 谓词下推未实现(PR #15385 仍 Draft,等 Spark 4.3)。1.4x 来自免去 JSON 文本解析
"存储节省 13.8% 来自 Shredding 拆列 + 字典编码" 部分错误。若未 shredded,节省来自二进制编码消除 JSON 语法开销。旁证:独立基准显示纯二进制编码(Spark 4.0,无 shredding)即可省 31.4%(10M 记录,279.32 → 191.58 MB)
"Variant + Shredding 适合聚合分析" 方向对,但 "+Shredding" 三个字应去掉
"未来下推完成后优势从 1.9x → 5-10x" 逻辑成立,这才是 shredding 的真正贡献(尚未兑现)。参考 Iceberg PR #16714 基准:下推 ON vs OFF 差 14.8×
好消息 这份报告测的是「Variant 二进制编码 vs JSON 文本解析」,不是「Shredding vs 不 Shredding」。结论方向(Agent Trace 类长 JSON 负载适合 Variant)依然可靠且有价值 —— 而且这个收益不依赖尚未落地的 shredding 下推,现在就能拿到

4.4 三组结果为何看似矛盾 —— 其实完全一致

关键:三组测试都没有用上 shredding 的列裁剪(connector gap 导致)。所以它们本质上都是在比两个运行时函数variant_get() vs get_json_object()。谁赢完全取决于工作负载:

负载特征JSON 文本解析成本Variant 二进制解析成本谁赢
短 JSON、字段少、路径浅、海量行逐行提取
游戏 detail:150 B / 6 字段 / 1 层

短串字符匹配极快
相对偏高
binary 头 + 元数据查找 + 类型转换
JSON
0.31x~0.78x
大表全表扫描聚合
COVID covidcast:2 亿行

逐行文本解析累积
不随行数放大
IO 优势叠加(少读 16%)
Variant
快 16~43%
长 JSON、字段多、路径深
Agent Trace:500–2000 B / 20+ 字段 / 2–3 层

长串线性扫描 + 深路径
不随长度 / 深度增长
offset 直接定位
Variant
1.38x~1.90x
统一规律
当前阶段的 Variant 查询优势 = f(JSON 字符串长度, 字段数, 路径深度, 需解析的行数)
                             —— 与 shredding 无关

真正的 Variant 加速(shredded 列裁剪,理论上可减少 IO 90%+)在三组测试里都还没体现。一旦 connector 实现下推接口,这三组数字都会被改写。


5. 配置踩坑:Iceberg 表属性 vs Spark session 配置

🚨 写 Iceberg 表时,不要用 spark.sql.variant.* 去开 shredding

这是两个完全独立、不可互换的配置通道:

  • spark.sql.variant.* → Spark 内置 Parquet data source 的开关
  • write.parquet.shred-variantsIceberg 的开关(默认 false,必须显式设置)

Iceberg 写入路径 Parquet.WriteBuilder.forTable() 只走 setAll(table.properties())完全不读 Spark session 的 variant 配置

叠加 Spark 对未知 spark.sql.* 键静默接受不报错这一行为,后果是:键名写错时你不会收到任何警告,表以未 shredded 方式写入,而你以为开了。我们的 Agent Trace 测试正是这么踩的。

正确的启用方式

-- 建表时
CREATE TABLE catalog.db.events (
  event_id BIGINT,
  payload  VARIANT
) USING iceberg
TBLPROPERTIES (
  'format-version'                              = '3',
  'write.parquet.shred-variants'                = 'true',
  'write.parquet.variant-inference-buffer-size' = '100'
);

-- 已有表开启(只影响后续写入的新文件)
ALTER TABLE catalog.db.events
SET TBLPROPERTIES ('write.parquet.shred-variants' = 'true');
Iceberg 表属性默认值说明
write.parquet.shred-variantsfalse启用 shredded 写入
write.parquet.variant-inference-buffer-size100schema 推断用的缓冲行数

配置优先级:DataSource Write Option > Spark Session Config(iceberg 前缀) > Table Property > Default

自查清单
  1. SHOW TBLPROPERTIES <table>; → 确认 write.parquet.shred-variants=true
  2. 读一个数据文件的 Parquet footer → 看 variant 列下是否有 typed_value 子列(未 shredded 只有 metadata + value
  3. EXPLAIN FORMATTED → 看 scan 的 Output 是否仍是完整 variant 列
方法论教训

任何 Variant 性能测试报告,如果没有给出 shredded 列数、Parquet schema 或 EXPLAIN 输出,那么它关于 shredding 的一切结论都只是推断。物理证据的门槛应该是:列数从 N 变成了 N+M


6. 上游进展追踪(截至 2026-07-29)

6.1 Iceberg 侧:实现已写好,但都还没合并

原先追踪的 issue #16448("SparkScanBuilder 未实现接口")现在已有对应实现 PR,作者均为 qlong:

PR / Issue内容状态
#16714 选择性 shredded variant Parquet readers —— 只读请求路径的 typed_value 列,不再物化整个 blob Open
3 轮 review 意见
等 review,目标 spark/v4.1
#16715 SupportsPushDownVariantExtractions 实现 —— SparkVariantExtractionScanBuilder,开关 spark.sql.iceberg.variant-extraction-push-down.enabled(默认开) Open
等 review
#16448 原始 issue(列裁剪 plan change) Open
2026-05-20 开
#16726 配套 issue:reader 侧只读选定 path Open
2026-06-08 开
#15384 variant extract API + manifest bounds 字节序修复(little-endian);被 #16714/#16715 依赖 Open
已获 +1,但被要求修改
#15385 variant_get 谓词下推用于 file skipping Draft
作者:"Will publish this
when Spark 4.3 is released"
#16133 Parquet row group skipping for shredded variant 已被 stale bot
自动关闭

2026-07-06

合并顺序:#16714 先 → #16715 后(端到端测试需要两者)。两个 PR 都还差至少一个 approving review。

#16714 自带的基准数据 —— 下推生效后确实翻盘

这是本轮调研最有价值的发现。作者用一天的 GitHub activity 数据(JSON + 299 个 shredded 列)做基准,取 3 次运行中位数:

配置总耗时相对基线
Variant + 下推 ON + 新选择性 readers(基线)49.74 s
JSON string63.66 s+28.0%(慢)
Variant + 下推 OFF735.39 s+1379%(慢 14.8×)

单查询最极端:c-q08 3.701 s(下推 ON)vs 87.059 s(下推 OFF),差 23 倍。

这组数字直接印证了我们的诊断
  • 「下推 OFF」那一行(慢 14.8×)就是我们实测环境的状态 —— 也解释了为什么我们的游戏日志测试中 variant_get 全面慢于 get_json_object
  • 一旦下推生效,Variant 从「慢 28%」变成「比 JSON string 快 28%」,攻守互换
  • 比率量级与我们的 0.31x~0.78x 不同,原因是负载差异(我们的游戏日志字段少、结构统一,JSON 解析本就极快,是 Variant 最不利的场景)

6.2 另一条线:向量化读取(#16292,已合并,但对 shredded 表无效)

PR #16292 — "Spark: Add vectorized Parquet reads for variant columns",作者 nssalian:

状态已合并(2026-07-19),milestone = Iceberg 1.12.0
做什么把 variant 的 metadata / value 两个子列作为 Arrow VarBinary 批量(向量化)读取。此前 variant 列会强制整表退化为 row-at-a-time 逐行读
改动arrow/ 新增 VectorizedVariantVisitor / VariantVectorHolderspark/v4.0 + v4.1 新增 VariantColumnVectorColumnVectorWithFilterVariantType 分支,使 variant 能与 DV / position delete 共存
关键:它对 shredded 表明确不生效

SparkBatch 里做了一个逐文件判断lowerBounds.containsKey(variantFieldId) —— 该 key 存在即说明是 shredded payload,此时主动关闭批量读、退回逐行。variant 列的 metrics mode 为 None/Counts 时同样退回。

对比维度#16292(已合并)#16714 + #16715(未合并)
解决的问题非 shredded variant 的逐行读开销shredded variant 的列裁剪 / 下推
手段Arrow 向量化批量读 value + metadata只读请求路径的 typed_value
对 shredded 表主动禁用向量化,退回逐行正是为其设计
我们 42 亿行实测受益无(我们的表是 shredded)会(基准显示 14.8×)

这与社区 Sync 纪要里「向量化读取器接近就绪,提升非裂解 Variant 读取性能」一条完全对应 —— 注意原文限定词就是「非裂解」。

结论:#16292 是好消息(Variant 基础读取性能改善,且是目前唯一确定进 1.12.0 milestone 的 variant 读取相关改动),但它不解决我们实测到的问题

关联:#16087(向量化 builder 的 variant 处理修复,其临时补丁被 #16292 移除);#16913ColumnVectorWithFilter 子向量修复,Open)。⚠️ 副作用需留意:#16292 让 variant 走上向量化路径并触及 DV / position delete 交互,而 #16913 仍 Open 说明这块 nested/child vector 语义还在收敛。若在 1.12 上跑 Variant + DV 组合,建议留一轮回归验证。

6.3 Spark 侧:4.2 已发布但不含 variant 性能修复

版本日期Variant 相关
Spark 4.12026-05-21Variant + shredding 写入 GA;定义 SupportsPushDownVariantExtractions 接口
Spark 4.2.02026-07-11 已发布⚠️ 未包含 variant 性能修复 —— #16715 中被追问过,作者已确认
Spark 4.3未发布🎯 关键版本
Ticket内容状态
SPARK-55617 VariantGet 加入 V2ExpressionBuilder,使 variant 谓词能经 DSv2 下推给 connector Resolved
Fix Version = 4.3.0
2026-06-15 解决
SPARK-55817 打通 Parquet row-group skipping for shredded variant(PushVariantIntoScan 产生的 v.`0` 逻辑路径无法映射到物理列,导致 filter 被丢弃) Open
Target 4.2.0 但未进
PR #54598 已被 stale bot
关闭(2026-06-13)

6.4 当前能拿到什么、拿不到什么

作者 qlong 在 #16715 中的表述,是目前最权威的分期判断:

能力时间线
Extraction pushdown
(列裁剪,少读列)
"can work today in simple cases" —— 只要 #16714 + #16715 合并,不必等 Spark 4.3;4.3 后自动进一步受益
File / row-group skipping
(少读文件)
"won't work until 4.3 is released" —— 依赖 SPARK-55617(已进 4.3)+ SPARK-55817(仍 Open

6.5 对既有预期的修订

原判断修订后
"Iceberg connector 未实现 SupportsPushDownVariantExtractions" 现象正确,但实现已存在于 PR #16714/#16715,尚未合并 —— 从「没人做」变成「做完了在等 review」
"读取加速尚未兑现" 仍然成立(截至 2026-07-29 无任何 release 含此能力)
"社区已明确列为 1.12(2026 Q3)目标" 需拆开看:Iceberg 最新 release 仍是 1.11.0(2026-05-20)。进了 1.12.0 milestone 的只有 #16292 向量化读取(非 shredded 路径);shredding 下推的 #16714/#16715 均无 milestone,不能保证进 1.12
隐含预期"等 Iceberg 1.12 就好了" 不成立。完整收益需 Iceberg 侧合并 + Spark 4.3,且 row-group skipping 的 Spark PR 已 stale 关闭,属最薄弱环节
⚠️ 一个必须留意的风险信号 三个关键 PR 被 stale bot 关闭或反复标记 stale(#16133 已关闭、SPARK PR #54598 已关闭、#15384/#15385 多次被标记后由作者手动救活)。这条线基本由 qlong 一人推动,#16714 至今 4 个参与者、0 个 approving review。这不是「即将落地」的形态,排期上不宜乐观。

7. AWS 侧支持矩阵

7.1 各服务 Variant 支持状态

服务VARIANT说明
Amazon S3 Tables GA
2026-07-28
存储 + 托管 Compaction 覆盖 Variant 列,15 个区域
EMR 8.0
Spark 4.0.2,2026-05-27 GA
GA 公告明确含 "VARIANT data types",EC2 / Serverless / EKS 三模式
EMR 7.12
Spark 3.5
不支持 Spark 3.5 无 VARIANT 类型,必须 Spark 4.0+
AWS Glue 部分 基础读写支持,Shredding 暂不支持
Athena(Trino) 不支持 暂不支持 Iceberg V3

7.2 S3 Tables 公告的正确解读

来源:AWS What's New, 2026-07-28

官方描述的工作机制三条:

  1. 任何 Iceberg V3 兼容引擎在写入时将半结构化数据 shred 到 hidden columns
  2. Shredding 产生 Parquet column statistics,引擎据此做 file pruning
  3. S3 Tables 的自动维护(含 Compaction)覆盖 Variant 列

可用区域(15 个):US(N. Virginia / Ohio / Oregon)、Asia Pacific(Mumbai / Seoul / Singapore / Sydney / Tokyo)、Canada(Central)、Europe(Frankfurt / Ireland / London / Paris / Stockholm)、South America(São Paulo)。

这条公告「新」在哪里

它不是「Variant 从不能用变成能用」。在此公告之前,用 OSS Spark 4.1.2 + Iceberg 1.11.0 通过 REST Catalog + SigV4 连 S3 Tables 写读 Variant 列已经可以跑通 —— 我们 2026-04-21 的验证确认建表、异构 JSON 写入、variant_get 提取、谓词过滤、混合聚合全部通过。

真正的增量是服务侧的正式支持,其中最关键的是:托管 Compaction 现在正式覆盖 Variant 列。此前 Variant 列走的是「引擎能写、但服务侧自动维护行为未经官方支持 / 文档化」的灰色状态,小文件治理需要自己兜。这一条解除了生产化的最后一个运维顾虑 —— 结合 §4.3 中「381 文件 vs 76 文件」导致的点查退化,这个价值不小。

⚠️ 公告的 file pruning 声称需要实测确认

公告称 shredding 产生的 Parquet column statistics 可用于 file pruning。这与本文「查询加速尚未兑现」的判断并不直接矛盾,因为它们是 §3 中的机制 ④ 和机制 ③,可以同时为真:公告讲的是少读文件,我们实测的瓶颈是读到的文件里仍要重建整个 Variant

但要注意:file pruning 在开源栈上恰恰是最薄弱的一环 —— 依赖的 SPARK-55817 仍是 Open,对应 Spark PR #54598 已被 stale bot 关闭(2026-06-13);Iceberg 侧的 row group skipping PR #16133 也已自动关闭(2026-07-06)。

因此公告若声称 file pruning 可用,可能来自 AWS 自有分支或补丁,不能假设与开源栈等价。在没有 S3 Tables 上的新实测数据前,不要把这条公告当作「shredding 读取加速已兑现」的证据。

7.3 Iceberg V3 逐特性支持(AWS)

服务Deletion Vectors + Row LineageVariant
EMR 7.12(Spark 3.5)GASpark 3.5 无 VARIANT
EMR 8.0(Spark 4.0.2)GAGA
S3 TablesGA
自动 Compaction 已 DV 感知
GA(2026-07-28)
托管 Compaction 覆盖,15 区域
Athena(Trino)暂不支持 V3
Spark 4.x 连 S3 Tables 的连接方式 必须走 Iceberg REST Catalog + SigV4s3-tables-catalog-for-iceberg 这个连接器是 Spark 3.x only,不适用于 Spark 4.x。

8. 已兑现的价值:运行时 Schema 自描述

虽然 shredding 查询加速没兑现,但有一块能力完全不依赖 shredding、现在就能拿到

8.1 核心机制

JSON String 列对引擎是不透明的 —— DESCRIBE table 只能看到 payload STRING,要知道里面有哪些字段只能靠采样。而 Variant 列一次 schema_of_variant_agg() 就能拿到精确字段名与类型,且随数据变化自动更新。

8.2 三个实测验证场景

环境:Phase 1 = 110 万行(100 万正常 + 10 万脏数据),Phase 2 增加 5 万行含新字段。测试方式为不允许探索性 SELECT * LIMIT N、仅凭 Catalog 元信息生成 SQL

场景JSON StringVariantVariant 优势
禁止采样时构造 SQL
只能读 Catalog 元信息
无法构造(不知字段) schema_of_variant 得到字段结构 零配置即可工作
Schema 演进
新增 match_result / match_score
元信息里看不到新字段 新字段立即可见 零维护成本
类型推断(含脏数据) 采样推断 → 遇 'N/A' 报错 精确类型 + try_variant_get SQL 正确率更高

COVID 实验进一步验证:一次 schema_of_variant_agg 即可拿到全部 8 种 source 的完整 schema,据此生成的 5 条业务 SQL 全部正确、零失败;而多宽表方案需 6 次 DESCRIBE 并自行判断字段语义是否等价,易出错。

这条能力的适用边界 它省掉的是采样探索这一步,适用于任何「消费方事先不知道 payload 结构」的场景 —— 多源数据汇聚、ad-hoc 探索、schema 频繁演进的管道。如果 payload 结构本来就固定且已知,这条价值为零,用宽表更合适。

9. 选型建议

场景建议理由
Ad-hoc 探索 / 消费方不知 payload 结构 现在就用 Schema 自描述省掉采样探索,不依赖 shredding 性能,当下即可兑现
多源异构数据 Landing / 汇聚 用 Variant 单表存多 schema,避免表爆炸,schema 演进零成本
长 JSON / 深路径 / 字段多的负载
如 OTel spans、Agent trace、API 日志
用 Variant 实测快 1.38x~1.90x,且此收益来自二进制编码,不等 shredding
大数据量全表扫描聚合 可用 Variant 实测 2 亿行下比 JSON String 快 16%~43%
短 JSON + 字段少 + 海量行逐行提取 暂不用 实测慢 0.31x~0.78x。这是 Variant 当前最不利的场景
高频点查 / 单列过滤(追求低延迟) 等下推落地 列裁剪下推未实现前,shredding 读取加速拿不到,甚至比 JSON 慢
高频固定 schema OLAP(Dashboard) 仍用宽表 列式 + 谓词下推性能最优
写多读少(日志高吞吐摄入) 关闭 shredding shredding 写入慢约 2.7×,且读取收益当前拿不到
大量分桶(bucket 数高 → 小文件) 反模式 文件降到 5.4 MB 时存储节省从 14.6% 塌到 0.65%
当前阶段的核心建议

不要为「shredding 查询加速」选 Variant。在当前栈(Spark 4.1.2/4.2 + Iceberg 1.11.0)上,选 Variant 的正当理由是三条已兑现的价值:

  1. 存储节省 13.8%~16%(前提:文件 > 50 MB)
  2. 运行时 Schema 自描述schema_of_variant_agg,省掉采样探索)
  3. 长 JSON 负载的二进制免解析收益(1.38x~1.90x)
一个反直觉的选项 若你的负载不依赖字段级列裁剪(整列取出、全表扫描聚合),Iceberg 1.12 的 #16292 向量化读取会带来收益 —— 但前提是表不 shredded。换言之短期内存在一个权衡:关掉 shredding 反而能吃到向量化批量读;开着 shredding 则既拿不到列裁剪(未合并)、又被排除在向量化之外(主动禁用)。

10. 待验证事项

10.1 确证 Agent Trace 表的 shredding 状态(按成本从低到高)

  1. 查表属性(1 分钟,决定性):SHOW TBLPROPERTIES tracelog.otel_spans_v2; —— 看 write.parquet.shred-variants 是否 = true;缺失或 false 即未 shredded
  2. 查 Parquet 物理 schema(决定性):读一个数据文件的 footer,看 attributes 下是否只有 metadata + value(未 shredded),还是另有 typed_value 子列(已 shredded)
  3. EXPLAIN FORMATTED 跑 Q1/Q3,确认 scan 的 Output 是否仍是完整 attributes 列
  4. 真正的 A/B:同一份数据写两张 Variant 表,唯一差别是 write.parquet.shred-variants true/false,重跑 Q1~Q5。这是唯一能隔离出 shredding 净效应的实验 —— 现有报告缺的正是这个对照组

附带价值:若 otel_spans_v2 确实未 shredded,那么升级到 Iceberg 1.12 后这张表能直接吃到 #16292 的向量化收益,而开了 shredding 的游戏日志表反而吃不到。这个反差值得在第 4 步的 A/B 中一并验证。

10.2 其他待验证

10.3 跟踪信号(按重要性)

  1. #16714 拿到 approving review → 最近的里程碑
  2. Spark 4.3 发布日期 + SPARK-55817 是否复活 → 决定 file skipping 能否兑现
  3. Iceberg 1.12.0 release notes → 预期含 #16292(向量化,非 shredded),不要误读为 shredding 下推已就绪
  4. #16913 合并情况 → variant + DV 的 child vector 语义是否收敛

附:资料来源

公开一手资料(设计初衷部分)

来源类型核对到的关键原文
Parquet VariantEncoding.mdSpec"efficiently queried by path";"each nested Variant value is contiguous and self-contained"
Parquet VariantShredding.mdSpec"data is often partially homogeneous";"Parquet's columnar representation for more compact data encoding, column statistics for data skipping, and partial projections"
SPARK-45891SPIP
作者 Chenhao Li
"repeated JSON parsing and degraded performance";"more efficient binary representation internally";"keeps the flexibility of schemaless JSON data"
Iceberg Issue #10392Proposal
2024-05-29,归入 V3 Spec
"efficient binary encoding of dynamic semi-structured data";"retain the flexibility of the source data, while allowing query engines to more efficiently operate on the data"
Databricks 工程博客
开源 Variant for Delta Lake & Apache Spark
厂商博客"Without Variant, customers had to choose between flexibility and performance.";"order of magnitude performance improvements"
Spark SQL Built-in Functions官方文档确认 schema_of_variantschema_of_variant_aggvariant_gettry_variant_getparse_json 为公开内置函数
AWS What's New
2026-07-28
官方公告S3 Tables 支持 Variant;shred to hidden columns;Parquet column statistics → file pruning;托管 Compaction 覆盖 Variant 列;15 区域

上述来源的动机 / 目标文本均已逐条核对。Snowflake 工程博客(Variant in Iceberg)抓取时返回 404,未能核到原文,故未列入。

上游 PR / Ticket

Iceberg: #16714 · #16715 · #16448 · #16726 · #15384 · #15385 · #16133 · #16292 · #16087 · #16913
Spark: SPARK-55617 · SPARK-55817 · PR #54598

本团队实测报告