3. Iceberg表结构基础

要真正掌握 Iceberg 的快照、Time Travel 与版本回溯,必须先理解它“表”的物理形态。Iceberg 与传统 Hive 表最大的区别在于:它不依赖目录扫描来推断数据,而是用一套自描述的元数据树精确记录“当前有哪些数据文件、它们属于哪个快照、由哪些历史版本演化而来”。这套元数据树自上而下依次是:元数据文件(metadata file)→ 清单列表(manifest list)→ 清单文件(manifest file)→ 数据文件(data file),而快照(snapshot)则是贯穿其中的版本锚点。

一、核心原理:五层对象的层次关系

Iceberg 的元数据是一棵有向的层次结构,每一层都指向下一层,最终落到真实存储的数据文件上。理解这棵树,就理解了 Iceberg 的一切版本能力。

  • 元数据文件(metadata file):通常是 v1.metadata.json、v2.metadata.json 这类 JSON 文件,是表的“总目录”。它记录表结构(schema)、分区规格(partition spec)、排序规则、当前快照 ID、所有历史快照列表,以及各快照对应的清单列表路径。每次提交都会生成一个新的 metadata 文件版本。
  • 快照(snapshot):表示表在某一时刻的完整状态,本质是一条指向某个清单列表的记录。快照有唯一 ID、时间戳、父快照 ID(parent-snapshot-id)和操作类型(append/overwrite/delete)。父快照链构成了版本回溯的路径。
  • 清单列表(manifest list):一个快照对应一个清单列表文件(如 snap-xxx.avro),它列出该快照包含的所有清单文件,并记录每个清单文件的分区范围、文件数量等统计信息,用于查询时快速裁剪。
  • 清单文件(manifest file):记录一批数据文件的详细条目,包括文件路径、格式、分区值、记录数、列级统计(min/max、null 数)以及该条目的状态(ADDED/EXISTING/DELETED)。它是 Iceberg 做文件级过滤和增量识别的关键。
  • 数据文件(data file):真正存储数据的 Parquet/ORC/Avro 文件,位于表的数据目录下,是查询最终读取的对象。
层次关系可以概括为:元数据文件 → 快照 → 清单列表 → 清单文件 → 数据文件。其中元数据文件是“根”,快照是“版本指针”,清单列表与清单文件是“索引”,数据文件是“实体”。Time Travel 只需切换元数据文件中的当前快照指针,无需移动或重写任何数据文件。

二、存储布局:目录结构与文件命名

在对象存储或 HDFS 上,一张 Iceberg 表的典型布局如下:

warehouse/db/orders/
├── metadata/
│   ├── v1.metadata.json
│   ├── v2.metadata.json
│   ├── snap-839201.avro        # 清单列表
│   ├── snap-839202.avro
│   ├── 3a1b2c-manifest.avro    # 清单文件
│   └── 7d4e5f-manifest.avro
└── data/
    ├── region=cn/part-00000.parquet
    ├── region=cn/part-00001.parquet
    └── region=us/part-00000.parquet

可以看到,元数据与数据分离:metadata/ 目录存放 JSON 元数据、清单列表和清单文件;data/ 目录存放真实数据文件。这种分离让元数据可以独立演进、快速提交,而数据文件一旦写入通常不可变(immutable),删除通过清单中的状态标记实现,而非物理删除。

三、案例场景:一次写入如何生成完整链条

假设对 orders 表执行一次追加写入,Iceberg 的提交流程大致如下:

  1. 写入新的 Parquet 数据文件到 data/ 目录。
  2. 为这批新文件生成一个清单文件,条目状态为 ADDED,并写入列级统计。
  3. 生成新的清单列表,它包含旧快照的清单文件(状态 EXISTING)和本次新增的清单文件。
  4. 创建新快照,指向该清单列表,并记录 parent-snapshot-id 为上一个快照。
  5. 生成新的元数据文件(如 v3.metadata.json),把 current-snapshot-id 指向新快照,并把新快照加入快照列表。

整个过程没有修改任何历史数据文件,只是新增了元数据。这正是 Iceberg 能低成本支持 Time Travel 的根本原因。

Iceberg 元数据层次与存储布局 元数据文件 metadata.json schema / spec / current-snapshot-id 快照 snap-839201 parent = null 快照 snap-839202 parent = 839201 快照 snap-839203 parent = 839202 清单列表 manifest list snap-839202.avro 清单文件 A 状态 EXISTING 清单文件 B 状态 ADDED data/part-00000.parquet data/part-00001.parquet data/part-00002.parquet data/part-00003.parquet current-snapshot-id 指向最新快照

四、误区与复盘:容易混淆的几个点

误区一:把快照当成数据副本。快照不是数据文件的拷贝,它只是一条指向清单列表的元数据记录。删除快照不会立即删除数据文件,只有过期快照清理(expire snapshots)后,不再被任何快照引用的数据文件才会被物理删除。
误区二:认为清单列表和清单文件是同一层。清单列表是“清单文件的清单”,一个快照对应一个清单列表;而一个清单列表可以包含多个清单文件,一个清单文件又包含多个数据文件。层级不能混淆。
复盘要点:排查“查询结果不对”或“Time Travel 找不到历史版本”时,先看 metadata/ 下最新的 metadata.json 中 current-snapshot-id 与 snapshots 列表是否完整;再顺着快照找到清单列表,确认清单文件状态标记是否正确。多数版本问题都出在元数据指针或清单状态上,而非数据文件本身。
层次对象 典型文件 核心作用 是否随每次提交新增
元数据文件 v3.metadata.json 表的总目录,记录 schema、快照列表与当前快照 是
快照 snap-839203(逻辑记录) 版本锚点,指向清单列表,含父快照链 是
清单列表 snap-839203.avro 列出快照包含的所有清单文件及分区统计 是
清单文件 3a1b2c-manifest.avro 记录数据文件路径、分区值、列统计与状态 视写入情况
数据文件 part-00002.parquet 真实存储数据,通常不可变 视写入情况

把这五层关系记牢,后续理解快照隔离、增量读取、版本回溯和元数据清理时,就能始终抓住“指针切换而非数据搬迁”这一 Iceberg 的核心设计思想。