---
url: /iceberg/1.12.0.md
description: >-
  Apache Iceberg 1.12.0 发布解析：Variant 向量化读取与 shredding 规则收紧、geometry/geography
  地理类型端到端落地、Flink  equality delete 转 deletion vector、希尔伯特曲线聚类、REST Catalog
  读限制，附真实案例与 Spark 4.1 上手。
---

# Apache Iceberg 1.12.0：777 次提交，只为把 JSON 和经纬度"扶正"

先讲两件看起来毫不相干的小事。

第一件。我认识一个做物流数据平台的朋友，他们的订单表里有个 `ext_info` 字段，装着业务方随手塞进去的 JSON。建表的时候图省事，定义成 `STRING`。然后每一次查询都要 `get_json_object()` 现解析。某天业务方问了一句："上周杭州仓的异常单里，payload 里 `reason` 是 `damaged` 的有多少？"他们跑了一整个晚上。

第二件。同一个公司另一个团队想算"每个站点周边 5 公里的订单量"。然后他们发现，这玩意的 SQL 里根本没有"经纬度"这个类型——要么手动写 haversine 公式硬算，要么把几亿行数据倒进 PostGIS 再捞回来。

这两个问题，一个叫**半结构化数据**，一个叫**地理空间数据**。它们在数据圈里存在了十几年，所有人都知道痛，但所有人都默认"本来就该这么麻烦"。

2026 年 9 月 29 日发布的 **Apache Iceberg 1.12.0**，一次性把这两件事都办了。

而我真正觉得有意思的是另一组数字：这个版本凝聚了 **777 次提交、142 位贡献者**——而它干的第一件和第二件大事，居然是"让 JSON 读得更快"和"让经纬度能存进去"。

> 一个不存数据、不算数据、只定义元数据规范的项目，却能让 Snowflake、Databricks、AWS、Google、Microsoft 五家坐在一张桌子上签字。这不是因为它野心大，恰恰是因为它克制——它只解决"数据在存储层怎么组织"这一件事，然后把这件事做到没人能绕开。

## 先把 Iceberg 是个啥说清楚

一句话版本：**Iceberg 是一个开放的数据湖表格式（Table Format）**。

它不是数据库，也不是查询引擎。它是一套"元数据规范"，规定了数据在对象存储上怎么组织、怎么被多个引擎安全地同时读写。数据文件还是那些 Parquet，但 Iceberg 在上面盖了一层分级的元数据：Catalog → Metadata file → Manifest list → Manifest file → Data file。

好处是什么？查询引擎不用 `LIST` 整个目录去找文件，靠元数据层层裁剪就能直接跳到目标文件；加列不会让历史数据变成"僵尸数据"；写坏了能回滚到上一个快照。

它 2017 年诞生于 Netflix，2020 年从 Apache 孵化器毕业。2026 年 5 月的 1.11.0 把 **V3 规范**推到了生产可用，而 1.12.0 是紧接着的那一刀——不是开新坑，是把 V3 从"规范上写明白了"变成"引擎里真跑通了"。

想系统补基础的，可以看我们之前写的[认识 Apache Iceberg](/iceberg/introduction)。这篇只聊 1.12.0 到底改了什么。

## 1.12.0 的六个真正值得关心的变化

先上总览，后面逐个拆：

| 方向 | 关键变化 | 谁受影响最大 |
| --- | --- | --- |
| Variant（半结构化） | 向量化读取、shredding 规则收紧、Flink/Kafka Connect 可写 | 日志、埋点、API 响应入库 |
| Geospatial（地理） | geometry / geography 读写打通，Spark 4.1 端到端 | 位置服务、物流、出行、GIS |
| Flink | ConvertEqualityDeletes、Iceberg view SQL、支持 Flink 2.2/2.3 | 实时入湖链路 |
| 数据布局 | 希尔伯特曲线（Hilbert curve）聚类策略 | 多维范围查询 |
| REST Catalog | 读限制（列掩码 + 行过滤）、unregister table、functions | 平台方、数据安全团队 |
| 地基 | V4 格式骨架、Jackson CVE 修复 | 所有人 |

### 一、Variant：半结构化数据不再是二等公民

先解释一个概念。**Variant** 是 Iceberg V3 引入的类型，专门用来装半结构化数据（JSON 那一类）。它的精髓在于 **shredding**（直译"撕碎"）：

把 JSON 里你经常查的那几个字段，"撕"出来存成真正的 Parquet 列；剩下的残部仍保留完整的二进制结构。这样过滤 `payload.severity = 'critical'` 时，引擎能直接用 Parquet 的列统计信息跳过整个文件，而不是把 JSON 一行行 parse 出来再比。

1.12.0 在这上面做了四件事：

* **读取快了**：Spark 4.0 和 4.1 上，未 shredded 的 Variant 列现在走**向量化 Parquet 路径**，不再是逐行解码
* **规则更严了**：shredding 从"多数决"改成**类型统一性**——一个字段只有当所有值都属于同一类型族（数值做宽化后）时才会被撕成类型列，混类型字段留在残部。之前那种"60% 是 int 就撕成 int 列、剩下 40% 覆盖不到"的情况没了
* **写入不只有 Spark 了**：Flink、Kafka Connect sink、generic record writer 现在都能产出 shredded Variant，Flink 还补上了 Avro 读写 Variant
* **一堆正确性问题被修掉**：大精度 decimal（precision > 18）shredding、UTF-8 字符串边界顺序、无统计信息列的 metrics 崩溃、畸形二进进制解析、ORC filter pushdown

最后那条清单看起来很无聊，但做过生产的人才知道：**半结构化数据的可怕之处从来不是常见情况，而是脏数据**。长字符串、null 满天飞的对象、超大 decimal、加密列——happy path 测试永远测不出这些。

关于性能，有个第三方实测值得参考：在 Iceberg 1.11 + Spark on EMR + S3 Tables 上，shredding 带来**约 34% 的读提速**，代价是**写入慢约 2.7 倍、存储涨约 20%**，21 个读模式里赢了 20 个（唯一输的是一个嵌套数组字段的过滤）。

> Shredding 本质上是一笔交易：用写入吞吐和存储空间换读取性能。一次写入、读几百次的表，闭眼开；一堆只为了合规留底、一周查两次的数据，开了就是给自己找不痛快。

### 二、Geospatial：经纬度终于是一等公民了

V3 规范里写了 `geometry` 和 `geography` 两种类型，但之前的 Java 实现基本是"能声明不能用"。1.12.0 把这条路铺完了：

* 两种类型都按 **WKB（Well-Known Binary）** 存储
* 在 Parquet 里映射到 **geometry / geography 逻辑类型**，文件自描述
* Avro 也能读写
* API 层补齐单值二进制序列化（默认值、元数据的场景）
* 会计算**几何包围盒（bounding box）指标**，写入时保留坐标参考系（CRS）信息

**Spark 4.1 是第一个端到端打通的引擎**：Parquet 读写两种类型，并且支持带地理列表上的行级 `DELETE` / `UPDATE` / `MERGE`（含 V3 的 merge-on-read、deletion-vector 路径）。

但官方也明说了当前的限制，别踩：

* 只有 Spark 4.1 支持，Spark 3.5 / 4.0、Flink、ORC 都还没有
* 读走的是 **row-based reader**，不是向量化读

一句话：这是第一个值得认真做端到端测试的 Java 版本，但**还不是**你可以直接升级共享表的信号。

### 三、Flink：实时入湖链路被补齐了几块砖

* **`ConvertEqualityDeletes` 维护任务**：把 equality delete 文件转换成 deletion vector。做过 Iceberg 实时入湖的人知道 equality delete 读放大有多难受，这个动作等于给了一条"事后优化"的官方路径
* **Iceberg view 支持 SQL**：视图能在 Flink SQL 里用了
* **引擎版本**：新增 Flink 2.2、2.3 支持，同时移除 Flink 2.0

### 四、数据布局：希尔伯特曲线聚类

`rewrite_data_files` 新增了**希尔伯特曲线（Hilbert curve）聚类策略**（Spark 4.1）。

为什么这个值得单独提？传统的排序（比如 Z-Order）在多维度上做范围查询时，邻近性保持得并不好。希尔伯特曲线是一种空间填充曲线，能把多维空间映射到一维同时**更好地保持空间局部性**——翻译成人话：对"经纬度 + 时间"这种多维范围扫描，聚簇效果通常比普通排序更好。

在 1.12.0 里同时落地希尔伯特曲线和地理类型，我倾向于认为这不是巧合。

### 五、REST Catalog 正在从"表元数据 API"变成"控制面"

这一块是 1.12.0 里最容易被忽略、但长期影响最大的部分：

* **Read restrictions（读限制）**：`loadTable` 响应里可以返回一个 `ReadRestrictions` 对象，描述必需的列投影（列掩码动作，比如只显示后四位、置 null、截断时间戳、哈希、字母数字掩码）和一个行过滤器。契约是**客户端强制 + fail-closed**：读端要么完整应用，要么直接让查询失败，绝不允许返回裸数据
* **Unregister table**：新增端点，移除表的注册信息但不删底层文件
* **Functions**：SQL 函数元数据的 list / load 端点进了 REST 契约
* **Catalog-side purge**：Spark 新增 `rest.catalog-purge`，让 `DROP TABLE PURGE` 把删除动作委托给 REST 服务
* **Catalog object identifiers**：统一标识符 schema，支持表和视图之外的对象

说白了：**身份、策略、语义、元数据本来就都在 Catalog 里交汇，那它自然就是该管权限的地方**。

> 顺带一句提醒：read restrictions 在 1.12.0 里只是规范和 OpenAPI 契约，**没有任何 Iceberg reader 真正执行它**。如果你的 Catalog 现在就返回了 ReadRestrictions，目前所有消费端都会静默忽略。别把它当成已经生效的防线。

### 六、地基：V4 的骨架和安全修复

* **Format V4** 打了地基：V4 manifest reader/writer、tracked-file adapters、相对路径、content statistics
* \*\*表达式规范（expressions spec）\*\*被采纳，谓词表达式有了引擎无关的正式定义，REST OpenAPI 同步对齐
* **行级血缘（row lineage）与 deletion vector 正确性**：修复 last-updated-sequence 继承、Spark manifest 重写时 first-row-ID 携带、Arrow 向量化读的直接内存泄漏；现在会拒绝"position delete 或 deletion vector 声称的分区与数据文件不匹配"的扫描
* **加密**：manifest-list 加密密钥随使用它的快照一起提交，Spark manifest 重写加密新写入的 manifest，新增 delegate 风格的 `EncryptingFileIO`
* **安全**：修掉 Jackson 相关 CVE（CVE-2026-54512、CVE-2026-54513、GHSA-r7wm-3cxj-wff9）

## 升级之前，先看这一节：会被打脸的地方

我每次看到 release notes 里 "777 commits" 都很开心，然后看到 "Breaking Changes" 就开始头疼。1.12.0 这块不算温柔：

**引擎支持变动**

* 移除 **Spark 3.4** 支持
* 移除 **Flink 2.0** 支持

**真的把 deprecated 的东西删了**（不是只打个警告）

* Spark：`SparkFilters`、旧 `SparkTableUtil` 方法
* Data：`GenericAppenderFactory`、`BaseFileWriterFactory`
* Core：`DataReader` 被移除，改用 `PlannedDataReader`
* AWS：旧的 S3 signer 类和属性
* 范围覆盖 Core、Data、Spark、Flink、Kafka Connect、BigQuery、AWS

**行为变更**

* `GeometryType` / `GeographyType` 的 `toString()` 现在总是带上解析后的 CRS：`geometry(OGC:CRS84)`、`geography(OGC:CRS84, spherical)`
* AWS SDK 默认 HTTP client 迁到 **Apache HttpClient 5**，自己带 AWS 依赖的要把 `software.amazon.awssdk:apache-client` 换成 `apache5-client`
* REST client 现在会对带 `Idempotency-Key` 的 POST 请求在可重试错误（408、500、502、503、504）上重试

一句话建议：**如果你维护过自定义的 catalog、FileIO、writer 或 Spark 扩展代码，先在 1.12 上编译一遍再排上线计划**。只跑运行时冒烟测试会漏掉那些只在维护路径或失败处理路径才加载的源码不兼容。

## 谁在真跑这些

Iceberg 不是那种"发布会上很热闹、生产里没人用"的项目。几个能查证的事实：

* **Netflix**：Iceberg 的诞生地。2023 年他们在 AWS re:Invent 上讲过那次 EB 级、纯 Iceberg 的迁移——约 **300 PB** 数据从 Hive 表整个搬走
* **早期大规模部署**：Apple、Airbnb、LinkedIn；**Pinterest** 在 S3 上用 Iceberg 管 PB 级用户与参与度数据
* **较新的采用者**：Bloomberg、Slack、Booking.com；**Spotify** 公开讲过用 Google Cloud 的 Iceberg 产品打通湖和仓
* **传统行业**：AT\&T 用 Iceberg 管理 PB 级通信数据，Shell 用它集中 IoT 与能源数据做共享

还有一份 2026 年 1 月的行业调研（Ryft 委托、TrendCandy 执行，样本 252 位生产环境使用 Iceberg 的数据负责人）：

* **58%** 用于业务关键分析
* **95%** 已在用或计划用 Iceberg 跑 AI/ML 负载
* **93%** 表示 Iceberg 解锁了此前做不了的新场景
* **79%** 计划在未来 12 个月内把剩余数据也迁到 Iceberg

> 表格式之战的结局，不是某个产品赢了，而是"开放"这个选项赢了。当五家云厂商都收敛到同一个格式，风险就反转了：问题不再是要不要用 Iceberg，而是你为什么还要把数据锁在只有一个厂商能读的格式里。

## 初体验：20 分钟摸一遍 Variant + 地理类型

用 Spark 4.1 本地跑一个最小闭环。拉起 spark-shell：

```bash
spark-shell \
  --packages org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.12.0 \
  --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
  --conf spark.sql.catalog.demo=org.apache.iceberg.spark.SparkCatalog \
  --conf spark.sql.catalog.demo.type=hadoop \
  --conf spark.sql.catalog.demo.warehouse=/tmp/iceberg-warehouse
```

建一张同时带 Variant 和地理类型的表：

```sql
CREATE TABLE demo.events (
  event_id     BIGINT,
  received_at  TIMESTAMP,
  payload      VARIANT,
  loc          GEOMETRY
) USING iceberg
PARTITIONED BY (days(received_at))
TBLPROPERTIES (
  'format-version' = '3',
  'write.parquet.shred-variants' = 'true'
);
```

注意两个开关。`format-version = 3` 是**强制的**，不给 VARIANT 列直接拒绝；`write.parquet.shred-variants = true` 才是真正产生隐藏类型列的开关，大多数发行版里它默认是关闭的。

写两条进去：

```sql
INSERT INTO demo.events VALUES
  (1, current_timestamp(),
   parse_json('{"user":{"id":42},"severity":"critical","reason":"damaged"}'),
   ST_GeomFromText('POINT(120.15 30.28)')),
  (2, current_timestamp(),
   parse_json('{"user":{"id":43},"severity":"info","reason":"none"}'),
   ST_GeomFromText('POINT(121.47 31.23)'));
```

然后查，体验一下"不再现解析 JSON"是什么感觉：

```sql
SELECT
  event_id,
  variant_get(payload, '$.user.id',  'int')    AS user_id,
  variant_get(payload, '$.severity', 'string') AS severity
FROM demo.events
WHERE variant_get(payload, '$.severity', 'string') = 'critical';
```

**`variant_get` 的第三个参数是被很多人忽略的关键**——给出期望类型，引擎才能直接绑定到 shredded 列上，省掉运行时的类型探测。不写也能查对，只是会走慢路径。

空间函数（比如 `ST_DWithin`、`ST_Distance`）由引擎和扩展提供，Iceberg 只负责类型定义与存储。这一点要分清楚：别把"能存"和"能算"混为一谈。

最后，看一眼你这张表到底 shredd 出了什么：

```sql
SELECT * FROM demo.events.files;
```

翻出某个数据文件的 Parquet schema 看看——逻辑上 4 列、物理上多出十几列，那就说明 shredding 在干活了。

## 进阶

写到这里，其实 1.12.0 最值得琢磨的不是那 777 次提交本身，而是它透露出的方向：**这个格式正在从"给慢速分析表设计的规范"，一点点往"能应付秒级提交、能管权限、能装地理和半结构化数据"的方向长**。V4 的相对路径和 content statistics、REST Catalog 的读限制、Variant 和地理类型——这几条线指向的是同一件事。

想继续往下挖的，我建议的顺序是：先去 [iceberg.apache.org](https://iceberg.apache.org/) 读完整的 1.12.0 release notes，再找一份你所在引擎的兼容性矩阵对着看，最后再动手升级。升级这件事上，保守永远比激进便宜。

更多开源技术干货和学习资料，关注公众号「遇码」，领取专属福利。
