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 用几乎一致的语言描述动机:
- Spark SPIP(SPARK-45891):用户依赖 JSON 表达式处理 JSON 数据,"can often lead to repeated JSON parsing and degraded performance";目标是 "use a more efficient binary representation internally and avoid repeated JSON parsing",同时 "keeps the flexibility of schemaless JSON data"。
- Iceberg Variant Proposal(Issue #10392):通过编码为 variant 列,"we retain the flexibility of the source data, while allowing query engines to more efficiently operate on the data"。
- Databricks 工程博客把取舍说得最直白:"Without Variant, customers had to choose between flexibility and performance."
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(部分投影) |
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 级跳过。
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 —— 无需采样即可知道有哪些字段及其类型 |
detail.map_id 这种写法在 Spark 4.1.2 上直接报错 INVALID_EXTRACT_BASE_FIELD_TYPE(Need 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 次正式取均值
存储
| 分桶策略 | JSON | Variant | 平均文件大小 | Variant 节省 |
|---|---|---|---|---|
| 无分桶 | 344.6 GB | 294.4 GB | 54 / 124 MB | 14.6% |
| bucket(8, aid) | 342.4 GB | 293.1 GB | ~55 MB | 14.4% |
| bucket(64, aid) | 251.2 GB | 249.6 GB | ~5.4 MB | 0.65% |
查询性能
| Query | 场景 | JSON (ms) | Variant (ms) | 比率 |
|---|---|---|---|---|
| Q1 | 单玩家 + map_id 过滤(点查 + 嵌套谓词) | 28,540 | 64,760 | 0.44x |
| Q2 | 时间范围 + map_type 过滤(分区裁剪) | 713 | 2,315 | 0.31x |
| Q3 | 单玩家按 map_id 聚合(3 次 variant_get) | 15,434 | 42,493 | 0.36x |
| Q4 | 多字段提取(5 个嵌套字段) | 24,645 | 58,352 | 0.42x |
| Q5 | 嵌套条件聚合 | 15,390 | 41,627 | 0.37x |
| Q6 | 1 天数据大范围聚合 | 11,782 | 15,147 | 0.78x |
单函数对照更干净:variant_get(detail, '$.map_id', 'STRING') = 40,120 ms vs get_json_object(detail, '$.map_id') = 27,844 ms。
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 劣势。
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,479 | 41,259 | 1.40x | Variant |
| Q2 | 全表聚合(所有 service 的 token 统计) | 34,451 | 65,417 | 1.90x | Variant |
| Q3 | 按 attribute 值过滤计数 | 26,775 | 36,977 | 1.38x | Variant |
| Q4 | 慢 span Top-100(LIMIT) | 359 | 180 | 0.50x | JSON |
| Q5 | trace_id 单点查询(调用链重建) | 55,074 | 36,132 | 0.66x | JSON |
存储:Variant 20.39 GB(381 文件 / 平均 54.80 MB)vs JSON 23.65 GB(76 文件 / 平均 318.60 MB),节省 13.8%(3.26 GB)。
原报告把 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.enabled | true | 允许 Parquet writer 写 shredded variant |
spark.sql.variant.allowReadingShredded | true | 允许 reader 读 shredded variant |
spark.sql.variant.pushVariantIntoScan | true | scan schema 中用 struct 替换 variant |
spark.sql.variant.shredding.maxSchemaWidth | 300 | 推断时最多 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 中未实现 |
| §未来预期 | "当社区完成 SupportsPushDownVariantExtractions…variant_get() 将被优化为只读取 shredded 列" | 这句自我否认了前两条 |
报告同时声称「列裁剪已生效」和「列裁剪尚未实现」,二者不能同时为真。同一套技术栈在两周前已被 EXPLAIN 证伪,所以正确的一半是「尚未实现」。
那 1.38x~1.90x 到底来自哪里?
来自机制 ① —— Variant 二进制编码免去 JSON 文本解析。有意思的是,报告自己的根因分析其实说对了机制,只是错误地挂在了「Shredding」名下:
| 因素 | 游戏 detail | Agent Trace attributes |
|---|---|---|
| JSON 字符串长度 | ~150 bytes | 500–2000 bytes |
| 字段数 | ~6 | 20+ |
| 路径深度 | 1 层($.gold) | 2–3 层($.gen_ai.usage.input_tokens) |
get_json_object()需线性扫描文本到目标路径 —— 字符串越长、路径越深越慢variant_get()通过 binary offset + metadata 字典直接定位 —— 路径深度不影响- 4.11 亿行 × 500~2000 字节的累积差异 → Variant 快 1.4x~1.9x
连带修正: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× |
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 配置
spark.sql.variant.* 去开 shredding
这是两个完全独立、不可互换的配置通道:
spark.sql.variant.*→ Spark 内置 Parquet data source 的开关write.parquet.shred-variants→ Iceberg 的开关(默认 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-variants | false | 启用 shredded 写入 |
write.parquet.variant-inference-buffer-size | 100 | schema 推断用的缓冲行数 |
配置优先级:DataSource Write Option > Spark Session Config(iceberg 前缀) > Table Property > Default
- Session 级:
spark.conf.set("spark.sql.iceberg.shred-variants", "true")— 注意是iceberg而非variant - Write option 级:
df.writeTo("table").option("shred-variants", "true").append()
SHOW TBLPROPERTIES <table>;→ 确认write.parquet.shred-variants=true- 读一个数据文件的 Parquet footer → 看 variant 列下是否有
typed_value子列(未 shredded 只有metadata+value) 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 string | 63.66 s | +28.0%(慢) |
| Variant + 下推 OFF | 735.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 / VariantVectorHolder;spark/v4.0 + v4.1 新增 VariantColumnVector;ColumnVectorWithFilter 加 VariantType 分支,使 variant 能与 DV / position delete 共存 |
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 移除);#16913(ColumnVectorWithFilter 子向量修复,Open)。⚠️ 副作用需留意:#16292 让 variant 走上向量化路径并触及 DV / position delete 交互,而 #16913 仍 Open 说明这块 nested/child vector 语义还在收敛。若在 1.12 上跑 Variant + DV 组合,建议留一轮回归验证。
6.3 Spark 侧:4.2 已发布但不含 variant 性能修复
| 版本 | 日期 | Variant 相关 |
|---|---|---|
| Spark 4.1 | 2026-05-21 | Variant + shredding 写入 GA;定义 SupportsPushDownVariantExtractions 接口 |
| Spark 4.2.0 | 2026-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 关闭,属最薄弱环节 |
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 公告的正确解读
官方描述的工作机制三条:
- 任何 Iceberg V3 兼容引擎在写入时将半结构化数据 shred 到 hidden columns
- Shredding 产生 Parquet column statistics,引擎据此做 file pruning
- 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 文件」导致的点查退化,这个价值不小。
公告称 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 Lineage | Variant |
|---|---|---|
| EMR 7.12(Spark 3.5) | GA | Spark 3.5 无 VARIANT |
| EMR 8.0(Spark 4.0.2) | GA | GA |
| S3 Tables | GA 自动 Compaction 已 DV 感知 | GA(2026-07-28) 托管 Compaction 覆盖,15 区域 |
| Athena(Trino) | 暂不支持 V3 | — |
s3-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 String | Variant | Variant 优势 |
|---|---|---|---|
| 禁止采样时构造 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 并自行判断字段语义是否等价,易出错。
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 的正当理由是三条已兑现的价值:
- 存储节省 13.8%~16%(前提:文件 > 50 MB)
- 运行时 Schema 自描述(
schema_of_variant_agg,省掉采样探索) - 长 JSON 负载的二进制免解析收益(1.38x~1.90x)
10. 待验证事项
10.1 确证 Agent Trace 表的 shredding 状态(按成本从低到高)
- 查表属性(1 分钟,决定性):
SHOW TBLPROPERTIES tracelog.otel_spans_v2;—— 看write.parquet.shred-variants是否 = true;缺失或 false 即未 shredded - 查 Parquet 物理 schema(决定性):读一个数据文件的 footer,看
attributes下是否只有metadata+value(未 shredded),还是另有typed_value子列(已 shredded) EXPLAIN FORMATTED跑 Q1/Q3,确认 scan 的Output是否仍是完整 attributes 列- 真正的 A/B:同一份数据写两张 Variant 表,唯一差别是
write.parquet.shred-variantstrue/false,重跑 Q1~Q5。这是唯一能隔离出 shredding 净效应的实验 —— 现有报告缺的正是这个对照组
附带价值:若 otel_spans_v2 确实未 shredded,那么升级到 Iceberg 1.12 后这张表能直接吃到 #16292 的向量化收益,而开了 shredding 的游戏日志表反而吃不到。这个反差值得在第 4 步的 A/B 中一并验证。
10.2 其他待验证
- S3 Tables 的 file pruning 是否真实生效 —— 在 S3 Tables 上复测单列查询,确认是否真的少读文件、以及是否已绕过 Variant 重建开销。这决定 AWS 是否用了自有分支
- cherry-pick #16714 + #16715 提前验证 —— 自建 Iceberg 跑我们的游戏日志数据集。#16714 基准显示下推 ON/OFF 差 14.8×,值得在我们自己负载上确认,尤其我们的负载「字段少 + 结构统一」是 Variant 最不利场景,验证价值高
- Variant + DV 组合在 1.12 上的回归 —— #16913 仍 Open,nested/child vector 语义还在收敛
10.3 跟踪信号(按重要性)
- #16714 拿到 approving review → 最近的里程碑
- Spark 4.3 发布日期 + SPARK-55817 是否复活 → 决定 file skipping 能否兑现
- Iceberg 1.12.0 release notes → 预期含 #16292(向量化,非 shredded),不要误读为 shredding 下推已就绪
- #16913 合并情况 → variant + DV 的 child vector 语义是否收敛
附:资料来源
公开一手资料(设计初衷部分)
| 来源 | 类型 | 核对到的关键原文 |
|---|---|---|
Parquet VariantEncoding.md | Spec | "efficiently queried by path";"each nested Variant value is contiguous and self-contained" |
Parquet VariantShredding.md | Spec | "data is often partially homogeneous";"Parquet's columnar representation for more compact data encoding, column statistics for data skipping, and partial projections" |
| SPARK-45891 | SPIP 作者 Chenhao Li | "repeated JSON parsing and degraded performance";"more efficient binary representation internally";"keeps the flexibility of schemaless JSON data" |
| Iceberg Issue #10392 | Proposal 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_variant、schema_of_variant_agg、variant_get、try_variant_get、parse_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
本团队实测报告
- · 游戏行为日志 Variant Shredding 性能报告(2026-06-01,42 亿行)
- · Agent Trace Log 性能测试报告(2026-06-15,4.11 亿行 OTel spans)
- · Iceberg V3 Variant 实验报告(COVID 多源数据,2 亿行)
- · Variant Schema 自描述验证(110 万行 + 5 万行 schema 演进)
- · S3 Tables Variant 功能验证(2026-04-21)
- · Apache Iceberg Variant 社区同步纪要(2026-06-04)
- · 独立基准:10M JSON 记录 STRING vs VARIANT 存储对比(279.32 → 191.58 MB,−31.4%)