2015年,Uber的工程师们面临一个看似无解的困境:他们的数据湖每天要处理数十亿次行程更新,每次行程的状态都可能从"进行中"变成"完成",甚至可能被取消。用传统数据湖的方式——把数据写入Parquet文件就不管了——根本行不通。每次更新都需要重写整个分区,删除操作更是噩梦。

三年后,Netflix的工程师们遇到了另一个问题:他们的Hive表在S3上已经积累了数百万个文件,每次查询前的目录列表操作就要花费几十秒。更糟糕的是,当用户修改查询条件时,因为没有使用正确的分区列,系统会进行全表扫描。

Databricks的团队则在思考一个更本质的问题:Spark作为一个强大的计算引擎,为什么不能像传统数据库一样支持事务?为什么写入失败会留下损坏的数据?为什么并发写入会导致数据丢失?

这三个团队的困境,实际上指向同一个技术空白:数据湖缺乏一个可靠的元数据层。这个空白催生了今天我们熟知的三大开放表格格式:Apache Hudi、Apache Iceberg和Delta Lake。

从文件集合到表格:元数据层的必要性

理解开放表格式的第一步是区分"文件格式"和"表格格式"。Parquet、Avro、ORC是文件格式,它们定义了数据如何在单个文件中组织。Parquet按列存储数据,非常适合分析查询;Avro按行存储,支持高效的序列化和Schema演化;ORC则是Hive生态系统的优化选择。

但文件格式解决不了一个根本问题:当你有上千个Parquet文件分布在数十个分区目录中时,如何知道哪些文件属于当前表?如何安全地添加、更新或删除数据?如何在并发读写时保证一致性?

Hive时代的答案是:目录结构。表的数据文件放在一个目录下,分区则用子目录表示。查询时,引擎通过文件系统API列出目录内容来确定表的状态。这种设计简单直接,但在云原时代暴露出致命缺陷。

对象存储的List操作成本高昂。AWS S3对每1000次List请求收费0.005美元,对于包含百万文件的表,一次完整列表可能消耗数十秒和可观的成本。更重要的是,List操作不是原子的——在列表过程中如果有新的文件写入,结果就会不一致。

目录结构的另一个问题是分区列与查询列的耦合。假设你有一个按月分区的销售表,分区列是event_month,但用户习惯按event_timestamp查询。在Hive模式下,如果查询不显式包含event_month条件,即使event_timestamp的范围完全落在某几个月内,引擎也无法利用分区信息,只能扫描全表。

flowchart TD
    subgraph Hive模式
        A[Hive表目录] --> B[/year=2023/]
        A --> C[/year=2024/]
        B --> D[/month=01/]
        B --> E[/month=02/]
        D --> F[data.parquet]
        D --> G[data2.parquet]
    end
    
    subgraph 查询流程
        H[SELECT * FROM sales] --> I[WHERE event_timestamp > '2024-01-01']
        I --> J[List目录获取所有文件]
        J --> K[扫描所有分区]
        K --> L[无法利用分区裁剪]
    end

开放表格式的核心创新在于:将表的元数据与物理文件解耦,用一个独立的元数据层来追踪表的状态。这个元数据层记录了表有哪些文件、每个文件的统计信息、Schema历史、分区配置等。查询时,引擎先读取元数据,根据查询条件确定需要扫描的文件,再读取实际数据。

这个看似简单的转变,带来了一系列强大能力:ACID事务、时间旅行、Schema演化、隐藏分区,以及对云原存储的深度优化。

三种元数据架构:从三层树到事务日志再到时间线

Apache Iceberg、Delta Lake和Apache Hudi采用了三种截然不同的元数据组织方式,每种都有其独特的设计哲学和权衡。

Iceberg的三层元数据树

Netflix设计的Iceberg采用了三层元数据架构,这种设计对云原存储极其友好。

最顶层是metadata.json文件,记录表的全局信息:当前Schema、分区配置、当前快照ID、历史快照列表等。这个文件是表的入口点,通常很小,可以快速加载。

每个快照对应一个Manifest List文件(Avro格式),列出了该快照包含的所有Manifest文件。Manifest List还存储了每个Manifest的分区范围统计,引擎可以用它快速跳过不相关的Manifest。

第三层是Manifest文件(同样是Avro格式),列出了具体的数据文件路径、文件大小、行数,以及每个列的统计信息(最小值、最大值、空值数量等)。这些统计信息是实现数据跳过(Data Skipping)的关键。

{
  "format-version": 2,
  "table-uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "location": "s3://bucket/data/events/",
  "last-updated-ms": 1677619248000,
  "current-snapshot-id": 1234567890123456789,
  "snapshots": [
    {
      "snapshot-id": 1234567890123456789,
      "timestamp-ms": 1677619248000,
      "manifest-list": "s3://bucket/metadata/snap-1234567890123456789-1.avro"
    }
  ]
}

这种三层设计有一个关键优势:分层裁剪。假设查询条件是WHERE event_date = '2024-01-15',引擎首先读取metadata.json找到当前快照,然后读取对应的Manifest List。Manifest List中每个条目记录了对应Manifest的分区范围,引擎可以跳过那些分区范围不匹配的Manifest。对于剩余的Manifest,引擎进一步检查其中每个数据文件的分区信息和列统计,最终确定需要扫描的文件。

对于一个包含10万个数据文件的大型表,如果查询只涉及一个分区,引擎可能只需要扫描几十个文件。这种分层过滤显著减少了元数据处理开销。

Delta Lake的事务日志与检查点

Databricks设计的Delta Lake采用了更接近Git的模型:一个不可变的事务日志。

每次对表的修改(插入、更新、删除、Schema变更等)都会生成一个新的JSON文件,命名为000000.json000001.json等。日志文件记录了这次操作添加或删除的文件、修改的配置等。

{
  "commitInfo": {
    "timestamp": 1629292910020,
    "operation": "WRITE",
    "operationParameters": {
      "mode": "Append"
    }
  },
  "add": {
    "path": "date=2024-01-15/part-00000.parquet",
    "partitionValues": {"date": "2024-01-15"},
    "size": 84123,
    "modificationTime": 1629292910020
  }
}

要重建表的当前状态,引擎需要从第一个日志文件开始,依次应用所有变更。但这在日志很长时效率低下——如果有10000个日志文件,读取和解析它们需要相当长的时间。

Delta Lake通过检查点解决这个问题。检查点是一个Parquet文件,记录了从表创建到某个版本的所有累积状态。引擎可以跳过检查点之前的所有日志,只处理检查点之后的增量日志。

Databricks的基准测试显示,对于有百万文件的表,使用检查点可以将元数据加载时间从数十秒减少到几秒。

Hudi的时间线机制

Uber设计的Hudi采用了时间线机制,将每次操作记录为一个带有时间戳的文件。

时间线文件有三种状态:requested(已请求)、inflight(进行中)、committed(已提交)。文件命名遵循[timestamp].[action].[state]的模式。例如,20240115120000.commit表示一个已提交的commit操作,20240115120100.compaction.requested表示一个已请求的压缩操作。

这种设计天然支持异步操作和并发控制。当两个操作尝试修改同一批文件时,后开始的操作会看到前一个操作的inflight状态,从而知道需要等待或调整自己的操作范围。

Hudi的另一个独特之处是它区分了Base Files和Log Files。Base Files是Parquet格式的完整数据文件,Log Files是Avro格式的增量变更文件。这种设计支持了Merge-on-Read存储模式,允许在写入性能和读取性能之间做权衡。

graph TB
    subgraph Iceberg架构
        I1[metadata.json] --> I2[Manifest List]
        I2 --> I3[Manifest 1]
        I2 --> I4[Manifest 2]
        I3 --> I5[Data File 1]
        I3 --> I6[Data File 2]
    end
    
    subgraph Delta Lake架构
        D1[_delta_log/] --> D2[000001.json]
        D1 --> D3[000002.json]
        D1 --> D4[000003.checkpoint.parquet]
        D1 --> D5[000004.json]
    end
    
    subgraph Hudi架构
        H1[.hoodie/timeline/] --> H2[commit文件]
        H1 --> H3[delta_commit文件]
        H1 --> H4[compaction.requested]
        H5[Base Files<br/>Parquet] --- H6[Log Files<br/>Avro]
    end

ACID事务:乐观并发控制的工程实现

传统数据库通过锁机制实现ACID事务,但在数据湖场景下,锁机制行不通。数据湖的数据规模可能是PB级别,一个事务可能运行数小时。持有锁这么长时间会严重阻塞其他操作。

三种表格格式都选择了乐观并发控制(Optimistic Concurrency Control,OCC)。OCC假设事务之间很少冲突,允许事务并行执行,只在提交时检测冲突。

以Delta Lake为例,事务流程如下:

读取阶段,事务读取表的当前版本和元数据。写入阶段,事务执行数据写入操作,将新文件写入存储。准备提交时,事务检查是否有其他事务已经提交了新版本。如果没有,原子地创建新的日志文件。如果检测到冲突,根据冲突类型决定是重试还是失败。

冲突检测的关键是确定哪些操作会冲突。两个事务写入不同的分区通常不会冲突。但如果两个事务都尝试修改同一批文件,就会产生冲突。Delta Lake通过分析事务的读写范围来判断冲突。

sequenceDiagram
    participant T1 as 事务1
    participant T2 as 事务2
    participant Log as 事务日志
    
    T1->>Log: 读取版本10
    T2->>Log: 读取版本10
    Note over T1,T2: 两个事务同时开始
    
    T1->>Log: 尝试写入000011.json
    Log-->>T1: 成功,版本变为11
    
    T2->>Log: 尝试写入000011.json
    Log-->>T2: 失败,版本已变为11
    
    T2->>Log: 重新读取版本11
    T2->>Log: 写入000012.json
    Log-->>T2: 成功,版本变为12

Iceberg的并发控制略有不同。Iceberg通过原子更新metadata.json文件中的current-snapshot-id来实现快照切换。写入事务创建新的快照(包括新的Manifest List和Manifest文件),然后尝试将metadata.json中的current-snapshot-id从旧值更新为新值。如果更新失败(因为其他事务已经修改了这个值),则说明发生了冲突。

Hudi的粒度更细。它支持文件组级别的并发控制,两个事务只要操作的是不同的文件组,就可以并行提交。这对于高频更新的工作负载特别重要。

乐观并发控制的一个潜在问题是写放大。当更新操作只影响一小部分数据时,Copy-on-Write模式需要重写整个文件。Hudi的Merge-on-Read模式通过将更新写入独立的Log Files来减少写放大。

存储模型:写时复制与读时合并

数据湖中的更新和删除操作是一个经典的工程难题。Parquet文件是不可变的——你不能修改一个已存在的Parquet文件中的一行数据。唯一的方法是重写整个文件。

这引出了两种基本的存储模型:Copy-on-Write(CoW,写时复制)和Merge-on-Read(MoR,读时合并)。

Copy-on-Write:简单但昂贵

CoW是最直观的方案。当需要更新某些行时,找到包含这些行的文件,读取文件内容,应用更新,写出新文件,在元数据中用新文件替换旧文件。

Delta Lake和Iceberg默认都使用CoW模式。这种模式实现简单,读取性能好(只需要读取Parquet文件),但写入代价高。假设一个1GB的Parquet文件包含100万行,你只需要更新其中的100行。CoW需要读取整个1GB文件,修改100行,然后写出新的1GB文件。写放大比达到10000:1。

CoW适合写入不频繁、读取频繁的场景。例如,每天一次的批量更新,或者以追加为主的日志数据。

Merge-on-Read:用读取复杂度换取写入性能

MoR将更新操作记录为增量文件,而不是立即重写基础文件。读取时,引擎需要合并基础文件和增量文件。

Hudi对MoR的实现最为成熟。它维护两种文件:Base Files(Parquet格式)和Log Files(Avro格式)。更新操作写入Log Files,读取时动态合并。

这种设计的权衡很明显:写入变快了(只需要追加增量文件),但读取变慢了(需要合并多份数据)。为了控制读取性能的下降,Hudi支持异步压缩,定期将Log Files合并到Base Files中。

压缩可以同步执行(在写入过程中)或异步执行(在后台独立进行)。异步压缩允许写入操作快速完成,但可能导致读取性能在压缩前下降。实际部署中,通常根据SLA要求来平衡压缩频率和读取性能。

flowchart LR
    subgraph Copy-on-Write
        A1[原始文件<br/>1GB Parquet] --> A2[读取+修改]
        A2 --> A3[新文件<br/>1GB Parquet]
    end
    
    subgraph Merge-on-Read
        B1[Base File<br/>Parquet] --> B3[读取时合并]
        B2[Log Files<br/>Avro] --> B3
        B3 --> B4[查询结果]
        
        B5[新更新] --> B6[追加到Log File]
    end
    
    subgraph 压缩过程
        C1[Base File] --> C3[合并压缩]
        C2[Log Files] --> C3
        C3 --> C4[新Base File]
    end

Iceberg在v2版本中引入了删除文件(Delete Files)来支持MoR。删除文件记录了需要删除的行,分为两种:等值删除(Equality Deletes)记录特定主键需要删除,位置删除(Position Deletes)记录特定文件中的特定位置需要删除。查询时,引擎先读取数据文件,再应用删除文件过滤结果。

Delta Lake在较新版本中引入了Deletion Vectors,类似Iceberg的删除文件机制。删除操作不再需要重写整个文件,只需要记录哪些行被删除。

选择CoW还是MoR取决于工作负载特征。如果更新操作只占写入总量的5%以下,CoW的简单性可能更重要。如果更新操作频繁,或者需要近实时的数据新鲜度,MoR的性能优势会更明显。

Schema演化与隐藏分区

数据Schema的变化是数据工程中的常态。新业务需求可能要求添加新列,数据质量改进可能要求修改列类型,合规要求可能要求删除某些敏感列。

传统Parquet文件对Schema演化的支持非常有限。添加列需要在所有文件中添加(或者接受新文件有列而旧文件没有),修改列类型几乎不可能,删除列意味着所有查询都需要处理缺失值。

Schema演化的兼容性规则

开放表格式通过元数据层来追踪Schema历史,允许不同版本的数据文件共存。但Schema演化不是无限制的——必须遵循兼容性规则。

兼容性规则确保读写操作的互操作性。向后兼容允许新Schema读取旧Schema写入的数据。向前兼容允许旧Schema读取新Schema写入的数据。

Iceberg支持最灵活的Schema演化:添加列(向后兼容)、删除列(向前兼容)、重命名列(双向兼容)、类型提升(如int到long,向后兼容)、修改列顺序。Iceberg使用字段ID而不是名称来匹配列,所以重命名操作不会影响数据读取。

Delta Lake支持添加列、删除列和修改列顺序,但对类型变更的支持有限。Hudi同样支持常见的Schema演化操作,还支持在写入时动态添加列。

类型提升是一个微妙的主题。将int提升为long通常是安全的,因为所有int值都可以表示为long。但将string提升为int就有风险,因为string可能包含无法解析为int的值。开放表格式通常只支持"安全"的类型提升。

隐藏分区:将分区细节对用户透明

传统Hive分区有一个令人沮丧的问题:用户必须知道表的分区列,并且在查询中显式使用它们。假设表按event_month分区,用户查询WHERE event_date = '2024-01-15',而不是WHERE event_month = '2024-01',引擎就无法利用分区裁剪。

Iceberg的隐藏分区解决了这个问题。分区定义可以基于列的变换函数,而不是列本身。例如,可以定义分区为month(event_timestamp)。用户查询WHERE event_timestamp BETWEEN '2024-01-01' AND '2024-01-31'时,Iceberg自动将这个条件转换为分区条件,实现分区裁剪。

-- 传统Hive分区
CREATE TABLE events_hive (
  event_id BIGINT,
  event_timestamp TIMESTAMP,
  event_data STRING
)
PARTITIONED BY (event_month STRING); -- 需要额外列

-- 用户查询必须使用分区列
SELECT * FROM events_hive 
WHERE event_month = '2024-01'; -- 才能利用分区

-- Iceberg隐藏分区
CREATE TABLE events_iceberg (
  event_id BIGINT,
  event_timestamp TIMESTAMP,
  event_data STRING
)
PARTITIONED BY (month(event_timestamp)); -- 变换函数

-- 用户可以查询原始列
SELECT * FROM events_iceberg 
WHERE event_timestamp BETWEEN '2024-01-01' AND '2024-01-31'; -- 自动分区裁剪

隐藏分区的另一个好处是分区演化。假设最初按月分区,后来发现数据量增长需要改为按日分区。在Hive模式下,这需要重写所有数据,将文件从月目录移动到日目录。在Iceberg中,只需要修改分区定义,新数据会按新分区写入,旧数据保持原有分区结构,查询引擎自动处理两种分区格式。

Delta Lake不支持隐藏分区,分区仍然需要显式列。但Delta Lake的Liquid Clustering(v3.1+)提供了另一种解决方案:不再需要预先定义分区,系统自动根据查询模式优化数据组织。

时间旅行与版本管理

每个数据工程师都经历过这种噩梦:一条错误的UPDATE语句执行了,数据被覆盖了。在传统数据湖中,这意味着数据永久丢失。在开放表格式中,时间旅行功能可以救你一命。

时间旅行允许查询表的历史版本。版本可以是数字版本号、时间戳,或者用户创建的标签。

Iceberg的每个快照都有唯一ID,可以通过snapshot-id参数查询特定快照。Delta Lake通过日志版本号实现时间旅行,支持VERSION AS OFTIMESTAMP AS OF语法。Hudi通过时间线记录所有变更,可以查询任意时间点的状态。

-- Iceberg时间旅行
SELECT * FROM events FOR SYSTEM_TIME AS OF '2024-01-15 10:00:00';

-- Delta Lake时间旅行
SELECT * FROM events VERSION AS OF 42;
SELECT * FROM events TIMESTAMP AS OF '2024-01-15 10:00:00';

-- Hudi时间旅行
SELECT * FROM events WHERE _hoodie_commit_time <= '20240115100000';

时间旅行的实现依赖于快照的保留策略。理论上,保留所有历史快照可以支持无限时间旅行,但存储成本会持续增长。实际部署中,通常会设置快照过期时间,定期清理旧快照和对应的数据文件。

Delta Lake的VACUUM命令会删除不再被任何快照引用的数据文件。默认保留7天历史,可以根据需要调整。Iceberg的expire_snapshots存储过程功能类似。Hudi的Cleaner服务会清理不再需要的旧版本文件。

时间旅行不仅是灾难恢复的工具,也是数据质量审计、机器学习实验复现、变更数据捕获(CDC)的基础能力。

小文件问题与压缩策略

当Spark Structured Streaming每5分钟写入一次数据,每次产生200个文件,一天就会产生57600个文件。如果这些文件每个只有几KB,查询性能会急剧下降。

小文件问题有三个层面的影响:元数据开销增加、查询规划变慢、对象存储API成本上升。

每次查询前,引擎需要读取表的元数据。对于Iceberg,需要解析Manifest List和多个Manifest文件。对于Delta Lake,需要读取日志文件或检查点。文件数量增加意味着元数据量增加,解析时间延长。

更隐蔽的影响在查询规划阶段。Spark需要为每个数据文件创建一个任务,调度大量小任务的开销可能比实际数据扫描时间还长。

对象存储的List和Get操作也按调用次数收费。读取100万个1KB文件比读取1000个1GB文件,API成本高出1000倍。

压缩:合并小文件

三种格式都提供了压缩机制来合并小文件。

Delta Lake的OPTIMIZE命令将小文件合并为目标大小(默认1GB)。可以指定分区范围,避免全表扫描。

OPTIMIZE events 
WHERE date >= '2024-01-01' 
ZORDER BY (event_type, user_id);

ZORDER BY是Delta Lake的多维聚类技术,按照指定列的Z序排列数据,使得经常一起查询的列在物理存储上更接近。

Iceberg的rewrite_data_files存储过程支持三种策略:binpack(简单合并)、sort(排序后合并)、zorder(Z序聚类)。

CALL catalog.system.rewrite_data_files(
  table => 'db.events',
  strategy => 'binpack',
  options => map('target-file-size-bytes', '536870912')
);

Hudi的压缩与存储模式紧密相关。对于MoR表,压缩是将Log Files合并到Base Files的过程。Hudi支持同步压缩(写入时)和异步压缩(后台执行)。异步压缩更复杂,但可以最小化写入延迟。

预防胜于治疗

比起事后压缩,更好的做法是在写入时就控制文件大小。

Spark的spark.sql.shuffle.partitions默认值是200,这对小数据集来说太大了。处理1GB数据时,200个分区意味着每个分区只有5MB。更好的做法是根据数据量和目标文件大小计算分区数,或者使用自适应查询执行(AQE)自动合并小分区。

// 自适应查询执行配置
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") // 128MB

Delta Lake的自动优化功能可以在写入后自动压缩文件。Hudi的小文件处理机制会在写入新数据时尝试填充已有的小文件,而不是创建新文件。

索引与数据跳过:减少IO的技术

在关系数据库中,索引是加速查询的核心机制。B树索引支持点查询和范围查询,位图索引适合低基数列,全文索引支持文本搜索。

但在数据湖场景下,传统索引的意义变了。数据湖查询通常扫描大量数据,而不是查询单行。索引需要服务于数据跳过——帮助引擎跳过不相关的文件或数据块。

列统计信息

最基本的"索引"是列统计信息。每个数据文件(或Parquet的Row Group)记录每列的最小值、最大值和空值数量。查询时,如果查询条件涉及某列,引擎检查该列的统计信息。如果查询条件完全落在统计范围之外,整个文件或Row Group可以被跳过。

统计信息的有效性取决于数据组织方式。如果数据按某列排序,该列的统计信息会非常紧凑——每个文件覆盖一个窄范围的值。如果数据是随机分布的,每个文件的统计范围可能覆盖几乎所有值,裁剪效果就很差。

布隆过滤器

布隆过滤器是一种概率数据结构,可以快速判断一个值"肯定不存在"或"可能存在"。相比列统计信息,布隆过滤器对点查询更有效。

假设查询条件是WHERE user_id = 12345。即使列统计信息显示某个文件的user_id范围包含12345,实际该文件可能并不包含这个值。布隆过滤器可以更精确地排除不相关的文件。

Iceberg和Delta Lake支持在数据文件级别存储布隆过滤器。Hudi的元数据表存储了布隆过滤器索引,可以加速记录级别的查找。

Hudi的多模态索引

Hudi的元数据表是一个独立的Hudi表,存储了多种索引:

Files Index记录文件的基本信息。Column Stats Index存储列的统计信息。Bloom Filter Index存储每个数据文件的布隆过滤器。Record Index(v0.14+)映射记录键到文件位置,支持高效的Upsert操作。

这种多模态索引设计使Hudi在高频更新场景下具有优势。当需要更新某条记录时,Record Index可以直接定位到包含该记录的文件,避免全表扫描。

flowchart TB
    subgraph 查询优化流程
        A[查询: WHERE user_id = 12345] --> B[检查分区条件]
        B --> C{分区裁剪}
        C -->|匹配| D[读取相关Manifest]
        C -->|不匹配| E[跳过分区]
        D --> F[检查列统计信息]
        F --> G{min/max裁剪}
        G -->|范围内| H[检查布隆过滤器]
        G -->|范围外| I[跳过文件]
        H --> J{可能存在?}
        J -->|是| K[扫描数据]
        J -->|否| I
    end

Z-Order与聚类

当查询涉及多个列时,单列排序或分区的效果有限。Z-Order是一种多维聚类技术,将多个列的值映射到一维空间,使得在多个维度上都相近的数据在物理存储上也相近。

Delta Lake的ZORDER BY和Iceberg的zorder压缩策略都实现了这种技术。对于涉及多个列过滤条件的查询,Z-Order可以显著提高裁剪效率。

Databricks的Liquid Clustering更进一步,系统自动选择最优的聚类列,并在数据增长时动态调整。这消除了手动设计分区和排序策略的需要。

生态与未来:开放表格式的战场

截至2024年,三种开放表格式都已发展成熟,但在生态支持上各有侧重。

Iceberg在多云和开放生态方面领先。AWS Athena、Google BigQuery、Snowflake都原生支持Iceberg。Iceberg的REST Catalog规范允许不同的Catalog实现(Polaris、Nessie、Glue等)互操作。

Delta Lake与Databricks平台深度绑定。在Databricks上使用Delta Lake能获得最佳体验,包括自动优化、预测性优化、Unity Catalog集成等高级功能。

Hudi在流式处理和高频更新场景占据优势。它的Upsert性能、增量查询、CDC支持使其成为实时数据湖的首选。

一个值得关注的发展是Apache XTable(原名OneTable)。这是一个互操作层,允许同一个数据集被当作Iceberg、Delta Lake或Hudi来读取。这消除了选择格式的锁定风险——你可以用一种格式写入,用另一种格式读取。

Snowflake的Polaris、Databricks的Unity Catalog OSS、AWS的S3 Tables都在推动开放表格式的标准化。未来的竞争可能不再是格式之争,而是Catalog和治理层的竞争。

flowchart TB
    subgraph 格式特性对比
        A[Apache Iceberg] --> A1[三层元数据架构]
        A --> A2[隐藏分区]
        A --> A3[开放生态领先]
        
        B[Delta Lake] --> B1[事务日志+检查点]
        B --> B2[Liquid Clustering]
        B --> B3[Databricks深度绑定]
        
        C[Apache Hudi] --> C1[时间线机制]
        C --> C2[MoR存储支持]
        C --> C3[流式处理优势]
    end
    
    subgraph 选择建议
        D[追加为主+批量分析] --> E[Iceberg]
        F[Databricks生态] --> G[Delta Lake]
        H[高频更新+实时处理] --> I[Hudi]
    end

选择哪种格式取决于具体场景。如果你的数据主要是追加写入,查询以批处理为主,Iceberg的开放生态和隐藏分区是加分项。如果你深度依赖Databricks生态,Delta Lake的集成优势明显。如果你需要处理高频更新和流式数据,Hudi的设计更适合。

无论选择哪种,开放表格式代表了数据湖从"文件集合"到"真正的表"的根本转变。这个转变带来的ACID事务、时间旅行、Schema演化等能力,正在重新定义数据工程的边界。


参考文献

  1. Apache Iceberg Official Documentation. Table Spec. https://iceberg.apache.org/spec/
  2. Databricks Blog. Diving Into Delta Lake: Unpacking The Transaction Log. 2019.
  3. Onehouse. Apache Hudi vs Delta Lake vs Apache Iceberg: Lakehouse Feature Comparison. 2024.
  4. Dremio Blog. Exploring the Architecture of Apache Iceberg, Delta Lake, and Apache Hudi. 2023.
  5. Dremio Blog. Apache Iceberg Hidden Partitioning Reduces Full Scans. 2022.
  6. AWS Blog. Apache Iceberg Optimization: Solving the Small Files Problem. 2023.
  7. Delta Lake Blog. Delta Lake Small File Compaction with OPTIMIZE. 2023.
  8. Jack Vanlightly. Beyond Indexes: How Open Table Formats Optimize Query Performance. 2025.
  9. Medium. Small Files, Big Problems: The Silent Killer of Data Lake Performance. 2026.
  10. Apache Hudi Blog. A Deep Dive on Merge-on-Read (MoR) in Lakehouse Table Formats. 2025.
  11. Alireza Sadeghi. The History and Evolution of Open Table Formats. 2024.
  12. Medium. Data Formats vs Table Formats: Hudi vs Iceberg vs Delta Lake vs Parquet vs Avro vs ORC. 2025.
  13. Starburst Blog. Iceberg Partitioning Best Practices. 2024.
  14. LakeFS Blog. Hudi vs Iceberg vs Delta Lake: Detailed Comparison. 2025.
  15. AWS Blog. Get a Quick Start with Apache Hudi, Apache Iceberg, and Delta Lake. 2022.
  16. Apache Iceberg Community. Manifest List and Manifest File Format Specification.
  17. Delta Lake Protocol. Transaction Log Specification.
  18. Apache Hudi Documentation. Timeline and Concurrency Control.
  19. Snowflake Engineering Blog. Apache Polaris Supports Apache Iceberg and Now Delta Lake. 2025.
  20. Conduktor. Iceberg Partitioning and Performance Optimization. 2024.
  21. Xenoss Blog. Apache Iceberg vs Delta Lake vs Hudi: Choose the Right Table Format. 2025.
  22. Atlan. Apache Hudi vs. Apache Iceberg: 2025 Evaluation Guide. 2025.
  23. Medium. Lakehouse Strategies: Copy-on-Write vs. Merge-on-Read. 2022.
  24. OLake Blog. Merge-on-Read vs Copy-on-Write in Apache Iceberg. 2025.
  25. Apache Arrow Blog. Querying Parquet with Millisecond Latency. 2022.
  26. Medium. All About Parquet: Metadata in Parquet. 2024.
  27. Capital One Tech. Delta Lake Transaction Logs Explained. 2025.
  28. Jack Vanlightly. Understanding Apache Iceberg’s Consistency Model. 2024.
  29. Apache Hudi Blog. 21 Unique Reasons Why Apache Hudi Should Be Your Next Data Lakehouse. 2025.
  30. Dremio Blog. Comparison of Data Lake Table Formats. 2022.
  31. Reddit r/dataengineering. Determining Iceberg v. Delta v. Hudi adoption? 2023.
  32. AWS Documentation. How Iceberg Works - Amazon EMR.
  33. Conduktor. Delta Lake Transaction Log: How It Works. 2026.
  34. Medium. Apache Iceberg vs Delta Lake: The Performance Wars of Modern Lakehouse. 2025.
  35. LinkedIn. Partitioning in Hive vs Delta vs Apache Iceberg. 2025.
  36. Dev.to. The Apache Iceberg Small File Problem. 2024.
  37. Starburst Blog. The File Explosion Problem in Apache Iceberg. 2025.
  38. Medium. Optimizing Compaction Parameters for Iceberg Tables. 2026.
  39. Dremio Blog. Open Table Formats and Object Storage: A Guide. 2025.
  40. Delta Lake Blog. Open Table Formats. 2024.
  41. Apache XTable Documentation. Interoperability Between Hudi, Delta, and Iceberg.
  42. Apache Hudi Documentation. Write Operations and Upsert Semantics.