三大数据湖表格式(Iceberg、Hudi、Delta Lake)全面解析与选型指南
- 编程
- 大数据
- 数据工程
目录
- 写在前面:我们到底在比较什么
- 背景:为什么需要表格式
- 传统 Hive 表的困境
- 表格式的使命
- Apache Iceberg:以”快照”为核心的表格式
- 核心结构:三层元数据树
- 快照(Snapshot)与原子提交
- 隐藏分区(Hidden Partitioning)
- 元数据级的文件裁剪
- 行级更新:MOR 与 COW
- Iceberg 的特点总结
- Apache Hudi:以”流式 upsert”为核心的表格式
- 核心概念:主键 + 索引
- 两种表类型:COW 与 MOR
- 关键机制:Timeline(时间线)
- 杀手锏:增量查询(Incremental Query)
- Hudi 的特点总结
- Delta Lake:以”事务日志”为核心的表格式
- 核心结构:事务日志 _delta_log
- 关键机制:基于日志回放的状态计算
- 功能特性
- Delta Lake 的特点总结
- 三者横向对比
- 核心维度对比表
- 三种元数据哲学的本质区别
- 选型指南
- 按场景选择
- 一个务实的判断顺序
- 一个常被忽视的现实
写在前面:我们到底在比较什么
讨论”三大数据湖”时,一个极其常见的误解是把它们当成”数据湖产品”。它们不是数据湖,而是”数据湖表格式(Table Format)”。 这个区分至关重要,是理解后面一切的前提。
- 数据湖(Data Lake) 是一个存储概念:用对象存储(S3、OSS、HDFS)以低成本存放海量原始文件。
- 数据湖表格式 是架在这些文件之上的一层”元数据 + 协议”,它让一堆散乱的 Parquet/ORC 文件,对外表现得像一张有事务、有 Schema、能更新、能时间旅行的数据库表。
换句话说,Iceberg / Hudi / Delta Lake 解决的是同一个核心问题:
对象存储上只有”文件”,没有”表”。怎么在不可变的文件之上,模拟出数据库才有的 ACID 事务、行级更新、Schema 演化和历史版本?
背景:为什么需要表格式
传统 Hive 表的困境
在 Iceberg/Hudi/Delta 出现之前,大数据世界用的是 Hive 表。Hive 的做法是:用一个目录代表一张表,用子目录代表分区,目录里放 Parquet 文件,元数据(有哪些分区)记在 Hive Metastore 里。
这套方案跑了很多年,但有几个致命缺陷:
1. 没有事务(ACID)。 多个作业同时写一张表,可能互相覆盖、产生脏数据。一个写作业跑到一半失败,会留下半截文件,下游读到不完整的数据。没有”提交”这个概念,写入不是原子的。
2. 改数据极其痛苦。 Parquet 文件是不可变的。要更新一行、删除一行(比如 GDPR 合规删除用户数据),唯一办法是把整个分区读出来、改完、整个覆盖写回。一个分区几十 GB,改一行要重写几十 GB。
3. 分区裁剪靠”猜目录”。 Hive 靠目录路径做分区裁剪(WHERE dt='2024-01-01' → 只扫 dt=2024-01-01/ 目录)。但这要求查询条件必须命中分区字段,而且 LIST 大量目录在对象存储上极慢(S3 的 list 操作又慢又贵)。
4. Schema 变更不可靠。 加字段、改字段类型、重命名列,Hive 处理得很脆弱,经常导致老数据读不出来或读错。
表格式的使命
三大表格式本质上都是为了根治上述四个问题而生。它们的共同思路是:
不再用”目录结构”来定义一张表,而是用一套独立的、版本化的”元数据文件”来精确记录:这张表此刻由哪些数据文件组成、每个文件的统计信息是什么、Schema 是什么。
数据文件依然是不可变的 Parquet/ORC,但”表 = 哪些文件的集合”这件事,被一层智能的元数据管起来了。每次写入产生一个新版本的元数据,旧版本保留——这就同时实现了原子提交(切换元数据指针)和时间旅行(读旧版本元数据)。
Apache Iceberg:以”快照”为核心的表格式
Iceberg 由 Netflix 发起,为解决其在 S3 上 PB 级数据的管理难题而生,后捐给 Apache。它的设计哲学是”开放、引擎中立、面向超大规模分析表”——不绑定任何计算引擎,不依赖 Hive Metastore 的目录假设。
核心结构:三层元数据树
Iceberg 的精髓在于它的元数据组织方式。一张 Iceberg 表的元数据是一棵树,从上到下三层:
metadata.json (表的当前状态:schema, 分区定义, 快照列表)
│
▼
manifest list (清单列表:本快照由哪些 manifest 组成)
│
▼
manifest file (清单文件:记录一批数据文件及其统计信息)
│
▼
data files (真正的 Parquet/ORC 数据文件)
- metadata.json:表的根元数据。记录当前 Schema、分区规则、以及所有快照(snapshot)的历史列表。每次写入,生成一个新的 metadata.json。
- manifest list:一个快照对应一个 manifest list,它列出这个快照包含哪些 manifest file,并记录每个 manifest 的分区范围统计。
- manifest file:记录一批 data file 的路径,以及每个文件的列级统计信息(每列的 min/max 值、null 数量、行数)。
- data files:实际的 Parquet 文件,不可变。

快照(Snapshot)与原子提交
每一次写操作,Iceberg 都会生成一个新的快照——一个完整记录”此刻表由哪些文件组成”的版本。提交的本质是:原子地把 metadata.json 里的”当前快照指针”从旧快照切换到新快照(通过 catalog 的原子操作保证,比如一次 CAS)。
这带来三个直接好处:
- 原子性:切换前读到的是旧版本完整数据,切换后读到的是新版本完整数据,绝不会读到中间状态。
- 时间旅行:旧快照不删,你可以查询”昨天下午 3 点这张表的样子”(
FOR TIMESTAMP AS OF/FOR VERSION AS OF)。 - 回滚:误操作了?把当前快照指针切回上一个快照即可。
隐藏分区(Hidden Partitioning)
这是 Iceberg 最受称道的设计。Hive 时代,分区字段是物理目录,用户查询必须显式写对分区条件才能裁剪。Iceberg 把分区变成了一个”从某列计算出来的逻辑转换”,记录在元数据里,对用户透明。
举例:你定义”按 event_time 的天分区”(days(event_time))。用户查询时只需写 WHERE event_time > '2024-01-01',Iceberg 自动推导出该裁剪哪些分区——用户根本不需要知道分区是怎么切的。而且分区方式可以演化(从按天改成按小时),老数据不用重写。
元数据级的文件裁剪
因为 manifest file 里记录了每个数据文件的列级 min/max 统计,Iceberg 在规划查询时,不需要 list 目录、也不需要打开数据文件,光读元数据就能判断”这个文件的 age 列范围是 20~30,而我查 age>50,那这个文件直接跳过”。
在对象存储上,这是巨大的性能优势——避免了 Hive 那种昂贵的 LIST 操作,把”找文件”从 O(目录遍历) 变成了 O(读元数据)。
行级更新:MOR 与 COW
Iceberg 支持两种更新模式(v2 格式):
- Copy-on-Write(COW,写时复制):更新一行时,把该行所在的整个数据文件读出、改、重写成新文件。写慢、读快(读时没有额外开销)。
- Merge-on-Read(MOR,读时合并):更新时不动原文件,而是单独写一个”删除文件(delete file)“记录哪些行被删/改。读取时把原文件和删除文件合并。写快、读慢(读时要做合并)。
Iceberg 的特点总结
- 引擎中立性最强:Spark、Flink、Trino、Presto、Hive、Impala 都能读写,是事实上的”开放标准”。
- 超大规模表的元数据性能优秀(百万级文件依然高效)。
- 隐藏分区和分区演化是独门优势。
- 相对而言,对”流式 upsert/CDC 入湖”的原生支持起步晚于 Hudi(但在快速追赶)。
Apache Hudi:以”流式 upsert”为核心的表格式
Hudi(Hadoop Upserts Deletes and Incrementals)由 Uber 发起。Uber 的核心诉求非常明确:把数据库的变更(CDC)近实时地、增量地灌入数据湖,并支持高效的 upsert(更新插入)。所以 Hudi 从第一天起就是为”流式增量写入 + 行级更新”而生的,这与 Iceberg”面向大规模分析查询”的出发点不同。
核心概念:主键 + 索引
Hudi 与 Iceberg/Delta 最本质的区别在于:Hudi 表有”主键(record key)“的概念,并维护一套”索引”。
为什么需要索引?因为 upsert 的核心难题是:当一条新记录进来,要判断它是”插入新行”还是”更新已有行”,就必须快速定位”这个主键之前写在哪个文件里”。Hudi 用索引(Bloom Filter 索引、HBase 索引、Bucket 索引等)来做这个”主键 → 文件”的映射,这是它能高效 upsert 的根基。
Iceberg/Delta 早期没有这种内建的主键索引机制,所以它们的 upsert 更多是”merge into”式的全表/分区扫描合并,在高频 upsert 场景下不如 Hudi。
两种表类型:COW 与 MOR
Hudi 把更新策略做成了表级别的两种类型(这是 Hudi 的标志性设计):
Copy on Write (COW) 表
- 更新时直接重写包含该记录的数据文件(Parquet)。
- 读取时直接读最新的 Parquet,读性能最好。
- 适合读多写少、对查询延迟敏感的场景。
Merge on Read (MOR) 表
- 更新时把变更写入增量日志文件(log file,行存格式 Avro),不立即重写 Parquet。
- 读取时分两种视图:
- Read Optimized 视图:只读已 compact 的 Parquet(快,但可能不是最新)。
- Real-time 视图:实时合并 base Parquet + log file(看到最新数据,但慢)。
- 后台异步执行 Compaction,定期把 log file 合并进 Parquet。
- 适合写多、要求低写入延迟的流式场景。
关键机制:Timeline(时间线)
Hudi 用一个叫 Timeline 的核心抽象来管理表上发生的所有动作。Timeline 是一串有序的”瞬间(instant)“,每个 instant 记录一个动作(commit、compaction、clean、rollback 等)及其状态(requested、inflight、completed)。
Timeline 是 Hudi 实现 ACID、增量查询、回滚的基础。它本质上类似数据库的事务日志,但记录的是表级别的操作历史。
杀手锏:增量查询(Incremental Query)
这是 Hudi 区别于其他两者的强大能力。基于 Timeline,Hudi 可以让你查询”从某个 commit 时间点之后,所有发生变更的记录”——而不是查询全表的当前状态。
-- 只拉取 commit 时间 > 某时刻的增量变更
SELECT * FROM hudi_table
WHERE _hoodie_commit_time > '20240101120000'
这让 Hudi 天然适合构建增量 ETL 管道:下游不用每次全量扫描,只处理上游新变化的部分,配合 InLong/Flink CDC 把数据库变更入湖后,下游层层增量加工。
Hudi 的特点总结
- 流式 upsert / CDC 入湖能力最强、最成熟(这是它的发家立命之本)。
- 主键 + 索引机制,高频更新场景性能优秀。
- 增量查询是独门绝技,适合增量 ETL。
- 表类型(COW/MOR)需要按场景预先规划,运维(compaction、clean)相对复杂,有一定学习曲线。
- 历史上与 Spark 绑定较深(但已在扩展多引擎支持)。
Delta Lake:以”事务日志”为核心的表格式
Delta Lake 由 Databricks(Spark 的母公司)开源。它的设计动机是:在 Spark 生态里,给数据湖加上可靠的 ACID 事务,让数据湖能像数仓一样可靠地支撑 BI 和 ETL。它与 Spark/Databricks 平台的整合是最丝滑的。
核心结构:事务日志 _delta_log
Delta 的全部魔法都在一个叫 _delta_log 的目录里。这个目录里是一串 JSON 文件(事务日志),每个 JSON 记录一次”提交(commit)“做了什么——增加了哪些文件、删除了哪些文件、Schema 变成了什么。
my_table/
├── _delta_log/
│ ├── 00000000000000000000.json (commit 0)
│ ├── 00000000000000000001.json (commit 1)
│ ├── 00000000000000000002.json (commit 2)
│ └── 00000000000000000010.checkpoint.parquet (检查点,加速)
└── (一堆 Parquet 数据文件)
- 每次写入 = 在
_delta_log里追加一个新的、编号递增的 JSON commit 文件。 - 这个 JSON 用 “Add File / Remove File” 的动作(action) 来描述表的变化。读表时,引擎按顺序回放这些 commit(从最近的 checkpoint 开始 + 后续 JSON),就能算出”当前表由哪些 Parquet 文件组成”。
- 每隔若干次 commit,生成一个 checkpoint(Parquet 格式的状态快照),避免每次都从头回放所有 JSON。
关键机制:基于日志回放的状态计算
理解 Delta 的关键是:表的当前状态 = 从某个 checkpoint 开始,依次回放后续所有 commit JSON 得到的结果。
- commit 5 说”添加文件 A、B”
- commit 6 说”删除文件 A,添加文件 C”
- 回放到 commit 6,当前表 = {B, C}
这种”事务日志 + 回放”的模型,和数据库的 WAL(预写日志)思想一脉相承,对熟悉数据库的人非常友好。原子提交靠”原子地创建下一个编号的 JSON 文件”实现(同名文件只能被一个写者成功创建,从而解决并发冲突)。
功能特性
- ACID 事务:基于日志的乐观并发控制(OCC)。
- 时间旅行:回放到任意历史 commit 版本(
VERSION AS OF/TIMESTAMP AS OF)。 - Schema 演化与强制(enforcement):写入时校验 Schema,防止脏数据污染表。
- MERGE / UPDATE / DELETE:支持标准 SQL 的行级操作(COW 为主,新版本逐步引入 deletion vectors 做 MOR)。
- 流批一体:可以同时作为 Spark Structured Streaming 的 source 和 sink,流和批读写同一张表。
Delta Lake 的特点总结
- 与 Spark / Databricks 生态整合无缝,在 Databricks 平台上体验和性能最佳。
- 事务日志模型简洁、易理解,运维相对简单。
- 流批一体能力强。
- 开源版(Delta Lake OSS)与 Databricks 商业版在部分高级特性上曾有差距(如 Z-Order、liquid clustering 等优化),不过开源版本在持续补齐。
- 引擎中立性历史上弱于 Iceberg(强依赖 Spark),但通过 Delta Standalone / delta-rs / UniForm 等在改善。
三者横向对比
核心维度对比表
| 维度 | Iceberg | Hudi | Delta Lake |
|---|---|---|---|
| 发起方 | Netflix | Uber | Databricks |
| 设计初衷 | 超大规模分析表、引擎中立 | 流式 upsert / CDC 入湖 | Spark 生态的 ACID 数据湖 |
| 元数据核心 | 快照 + 三层 manifest 树 | Timeline + 索引 | 事务日志 _delta_log |
| 原子提交方式 | 切换元数据指针(catalog 原子操作) | Timeline instant 状态流转 | 原子创建递增编号的 JSON |
| 行级更新 | COW / MOR(v2) | COW / MOR(表类型) | COW 为主(渐进 deletion vectors) |
| 主键 & 索引 | 无内建主键索引 | 有,核心能力 | 无内建主键索引 |
| 增量查询 | 支持(incremental read) | 最强,独门绝技 | 支持(CDF, Change Data Feed) |
| 隐藏分区 | 支持,独门优势 | 不支持(需显式) | 不支持(用 generated column 近似) |
| Schema 演化 | 强(含分区演化) | 支持 | 强(含 enforcement) |
| 时间旅行 | 支持 | 支持 | 支持 |
| 引擎中立性 | 最强 | 中(Spark 为主,扩展中) | 中(Spark 为主,扩展中) |
| 流式写入成熟度 | 良好 | 最成熟 | 良好(流批一体) |
三种元数据哲学的本质区别
如果只记一件事,请记住三者元数据模型的根本差异——这决定了它们各自的性格:
- Iceberg = “快照思维”:每次提交是一个完整的世界快照,用分层 manifest 高效组织海量文件的统计信息。→ 擅长大规模分析查询的文件裁剪。
- Hudi = “记录思维”:以主键和索引追踪每一条记录的位置,用 Timeline 管理变更历史。→ 擅长高频行级 upsert 和增量消费。
- Delta = “日志思维”:用一串事务日志记录每次”加了/删了哪些文件”,回放即得当前状态。→ 擅长简洁可靠的事务,与 Spark 无缝协作。
选型指南
按场景选择
选 Iceberg,如果:
- 你要建一个引擎中立的数据湖,未来可能用 Spark、Flink、Trino 多种引擎查询,不想被绑定。
- 表规模极大(PB 级、百万文件级),需要顶级的元数据查询性能。
- 你重视隐藏分区、分区演化这些能让查询”傻瓜化”的特性。
- 典型:大型企业统一数据湖底座、对外开放的湖仓架构。
选 Hudi,如果:
- 你的核心场景是数据库 CDC 近实时入湖,有大量高频 upsert。
- 你需要增量 ETL——下游只处理上游的增量变更。
- 写入延迟敏感(用 MOR 表)。
- 典型:用 InLong/Flink CDC 把 MySQL binlog 实时入湖、构建增量数仓。
选 Delta Lake,如果:
- 你的技术栈重度依赖 Spark / Databricks。
- 你想要简洁可靠的 ACID 事务,团队熟悉”事务日志”心智模型。
- 需要强流批一体(同一张表既被流写也被批读)。
- 典型:Databricks 平台上的 Lakehouse、Spark 主导的 BI/ETL 体系。
一个务实的判断顺序
- 先看生态绑定:已经深度用 Databricks → Delta;要多引擎开放 → Iceberg。
- 再看核心负载:主场景是高频 CDC upsert + 增量消费 → Hudi。
- 趋势参考:近两年 Iceberg 的社区势头和厂商支持(含 Snowflake、Databricks 通过 UniForm 兼容、各云厂商)增长最猛,正在成为”开放湖仓”的事实标准;但在纯流式 upsert 场景,Hudi 依然有成熟度优势。
一个常被忽视的现实
三者正在互相趋同:Iceberg 在补强 upsert,Hudi 在扩展多引擎,Delta 在增强开放性(UniForm 甚至能让一张表同时被当作 Delta 和 Iceberg 读)。所以选型不必焦虑”赌错技术”——把数据存成开放的 Parquet、用标准 SQL 操作,迁移成本是可控的。真正决定体验的,往往是你的计算引擎和云平台生态,而不是表格式本身的功能清单。