表格式详解:V1 vs V2 格式,何时选择 V2?

好,咱们进入第三章。说实话,V1 和 V2 的格式选择,是我在咨询中被问得最多的问题之一。很多人觉得「V2 肯定比 V1 好,无脑上 V2 就对了」。嗯,事情没那么简单。

我见过不少团队,为了追新把 V1 表强行升级到 V2,结果发现自己的场景根本用不上 V2 的特性,反而多了一堆维护成本。今天我就把这两个版本的底裤扒干净,告诉你什么时候该选 V2,什么时候老老实实用 V1 反而更香。

V1 和 V2 到底差在哪?

先看一张总览图,把两个版本的核心差异刻在脑子里。

Iceberg V1 vs V2 核心差异 V1 格式 行级删除 删除标记为墓碑文件,读时过滤 快照隔离 基于 Manifest 列表的 MVCC Schema 演进 支持列添加/删除/重命名 分区演进 支持分区变更,不重写数据 ❌ 不支持行级 UPDATE/DELETE 只能全文件覆盖 V2 格式 行级删除(Delete Files) Position Delete / Equality Delete 行级 UPDATE/DELETE MERGE INTO / DELETE FROM 严格 Schema 校验 写入时强制类型匹配 快照引用完整性 Manifest 列表必须引用有效快照 ⚠️ 写入性能略低 Delete File 合并开销 演进

V1 格式:简单可靠的老伙计

V1 是 Iceberg 的第一个稳定格式。它的设计哲学很纯粹——只做追加写。你想想看,在数据湖场景里,大部分数据都是日志、事件、点击流这种只增不减的东西。V1 就是为这个场景量身定做的。

V1 的删除机制很原始。要删一行数据?它不会真的去改 Parquet 文件,而是在 Manifest 里标记这个文件「部分失效」。读的时候跳过这些文件。说白了,就是「假装删掉了」。

V1 的核心优势:

  • 写入路径极简,没有 Delete File 的合并开销
  • 读取性能稳定,不需要在运行时合并 Delete File
  • 兼容性最好,几乎所有 Iceberg 引擎都完美支持
  • 元数据体积小,没有额外的删除记录文件

我在一个日增 10TB 的日志平台项目里,用的就是 V1。为什么?因为数据只追加,从不修改。V1 的写入吞吐能跑到单节点 200MB/s,V2 在同样硬件下只能跑到 150MB/s 左右。这 25% 的性能差距,在规模大了以后就是真金白银。

V2 格式:为变更数据捕获而生

V2 最大的改进,就是引入了 Delete File 的概念。它有两种类型:

Delete File 类型 工作原理 适用场景 性能特点
Position Delete 记录 (文件路径, 行号) 对,精确标记要删除的行 小批量精确删除,CDC 场景 读取时需合并,有额外 IO
Equality Delete 记录被删除行的主键或等值条件 基于主键的 UPSERT,去重 需要哈希匹配,CPU 开销较大

有了 Delete File,V2 就能支持真正的 MERGE INTO 和 DELETE FROM 了。你想想看,在实时数仓里,你要做缓慢变化维(SCD),或者从 Kafka 消费 CDC 数据做回放——没有行级更新根本玩不转。

我的经验:如果你用 Flink CDC 同步 MySQL 到 Iceberg,必须用 V2。Flink 的 Changelog 流会产生大量的 UPDATE_BEFORE 和 DELETE 消息,V1 根本处理不了。我曾经见过一个团队用 V1 接 CDC,结果数据重复率高达 30%,排查了三天才发现是格式问题。

什么时候选 V2?一张决策表搞定

别纠结了,直接看这张表。我根据实际项目踩过的坑,给你列了 6 个典型场景:

场景 推荐格式 理由
日志/事件流(只追加) V1 写入性能高 20-30%,无 Delete File 开销
CDC 实时同步(MySQL → Iceberg) V2 必须支持行级 DELETE/UPDATE
维度表(缓慢变化维) V2 需要 MERGE INTO 做 SCD Type 2
批量 ETL(全量覆盖) V1 INSERT OVERWRITE 在 V1 下更高效
数据湖上的 OLAP 查询 V1 读取时无需合并 Delete File,查询更快
湖仓一体(需要 ACID 事务) V2 V2 的快照引用完整性更强

⚠️ 避坑指南:我曾经帮一个客户做格式选型,他们的场景是「每天全量覆盖一次,但偶尔需要修正几条历史数据」。他们选了 V2,结果每次修正都会产生 Delete File,三个月后 Delete File 的数量超过了数据文件本身,查询性能下降了 60%。

正确的做法是:主体用 V1,修正操作通过分区覆盖 + 重跑 ETL 来实现。如果修正频率极低(比如一个月一次),完全没必要上 V2。

V2 的隐藏成本:Delete File 合并

很多人只看到 V2 的功能强大,没看到它的代价。V2 的 Delete File 不会自动消失,它们会不断累积。你每次做 UPDATE,都会生成新的 Delete File。读数据的时候,Iceberg 需要把数据文件和 Delete File 做合并。

这个合并操作有多贵?我拿一个真实案例给你看:

-- 假设有一张 1TB 的表,每天有 100 万行更新
-- 30 天后,Delete File 的总大小可能达到 50GB
-- 每次全表扫描,需要额外读取 50GB 的 Delete File
-- 并且要在内存中做行级合并

-- 解决办法:定期执行压缩(Compaction)
CALL catalog.system.rewrite_data_files(
  table = 'db.orders',
  options = map(
    'rewrite-all', 'true',
    'delete-file-threshold', '10'  -- 当 Delete File 超过 10 个时触发
  )
);

嗯,这里要注意。压缩操作本身也很贵,它会把数据文件和 Delete File 合并成新的数据文件。我建议你在业务低峰期做,比如凌晨 3 点。而且不要每天都做,根据你的更新量来定——我一般建议 Delete File 数量超过数据文件数量的 20% 时再触发。

格式升级:从 V1 到 V2 的迁移路径

如果你已经在用 V1,想升级到 V2,可以。但别直接 ALTER TABLE,那是个坑。

正确的做法是:

  1. 先评估必要性——你真的需要行级更新吗?
  2. 创建新表——用 V2 格式建一张新表
  3. 迁移数据——用 INSERT INTO 把 V1 表的数据全量写入 V2 表
  4. 切换应用——改数据源指向,旧表保留一段时间做回滚

为什么不建议原地升级?因为 V1 表的 Manifest 结构和 V2 不兼容。原地升级会导致历史快照全部失效,一旦回滚就完蛋了。我见过有人直接在生产环境跑 ALTER TABLE ... SET FORMAT_VERSION 2,结果查询直接报错,回滚都回不去——因为 V2 的写入器不认 V1 的 Manifest 格式。

小结:选型三问

最后,给你一个简单的自检清单。选格式之前,问自己三个问题:

  • 我的数据会修改吗?——如果只追加,V1 更香
  • 修改频率有多高?——每天超过 1% 的行变更,考虑 V2;否则 V1 + 定期重刷
  • 我的查询对延迟敏感吗?——如果要求秒级响应,V1 的读取性能更稳定

说白了,V2 是个好技术,但它不是银弹。选对格式,比用对格式更重要。我在项目里见过太多「为了用 V2 而用 V2」的案例,最后都付出了性能代价。记住一句话:工具服务于场景,不是场景服务于工具。