2、核心概念速览:表、快照、清单、数据文件的关系图谱

好,咱们直接进入正题。Iceberg 的四个核心概念——表(Table)、快照(Snapshot)、清单(Manifest)、数据文件(Data File)——说白了就是一套层层递进的元数据索引体系。我刚开始接触时也觉得绕,但一旦理清它们之间的“父子关系”,后面所有操作都会变得很自然。

2.1 一句话概括各自职责

先给个速记版:

  • 表:逻辑容器,你写 SQL 时操作的对象。它不存数据,只存元数据指针。
  • 快照:表在某个时刻的完整视图。每次提交(INSERT/UPDATE/DELETE)都会生成一个新快照。
  • 清单:快照的“目录文件”,记录了一组数据文件的元信息(列统计、行数、路径等)。
  • 数据文件:真正存数据的 Parquet/ORC/Avro 文件,躺在 HDFS 或 S3 上。

嗯,这里要注意:快照不是数据拷贝,它只是一个指针列表。所以 Iceberg 做时间旅行时,不会像 Hive 那样把整个分区目录复制一遍。

2.2 它们是怎么串起来的?

我画了一张关系图,帮你直观理解这个层级结构。你看完应该就能明白为什么 Iceberg 的查询可以做到“无锁并发”。

核心关系链:

表 → 指向当前快照 → 快照指向一组清单 → 每个清单指向一组数据文件

Iceberg 核心概念关系图谱 Iceberg 核心概念关系图谱 表 (Table) 指向当前快照 快照 (Snapshot) 快照ID: 1234567890 | 提交时间: 2025-01-15 10:30:00 包含 N 个清单 清单列表 (Manifest List) 清单 A 清单 B 清单 C 数据文件 (Data Files) file_1.parquet file_2.parquet file_3.parquet file_4.parquet ...

我的经验: 刚上手时,可以把“快照”想象成 Git 的 commit。每次提交只记录变更的文件,而不是复制整个仓库。Iceberg 的快照也是同理——它只记录新增或删除的清单文件,所以生成快照的成本极低。

2.3 每个概念的详细拆解

2.3.1 表(Table)

表是 Iceberg 对外暴露的唯一接口。你在 Spark、Flink、Trino 里写 SELECT * FROM my_table,实际上操作的就是这个逻辑对象。

表的核心元数据存储在 metadata 目录 下,包含:

  • 表的 Schema(列名、类型)
  • 分区策略(Partition Spec)
  • 当前快照的指针
  • 所有历史快照的列表

我个人习惯把表看作“一本账本”,每次修改都在账本上记一笔,而不是把整本账本重写一遍。

2.3.2 快照(Snapshot)

快照是 Iceberg 实现 ACID 和时间旅行的基石。每次写入操作(INSERT/UPDATE/DELETE/MERGE)都会生成一个新的快照。

快照里存了什么?说白了就是一个 清单列表的指针,外加一些元信息:

快照属性 说明
snapshot-id 全局唯一的快照 ID,单调递增
timestamp-ms 快照生成时间(毫秒级)
manifest-list 指向清单列表文件的路径
summary 操作类型、影响行数等统计信息
parent-snapshot-id 父快照 ID,用于构建快照链

我曾经踩过的坑: 有一次在生产环境做大批量删除,忘了清理过期快照。结果 metadata 目录下积累了上千个快照文件,导致每次查询都要扫描整个快照链,性能直接掉到脚踝。后来我养成了定期执行 expire_snapshots 的习惯。

2.3.3 清单(Manifest)

清单是 Iceberg 元数据分层中最容易被忽视、但也是最有设计巧思的一层。

每个清单文件(Manifest File)本质上是一个 数据文件的索引,里面记录了:

  • 数据文件的路径(location)
  • 文件格式(Parquet/ORC/Avro)
  • 分区信息(partition data)
  • 列级别的统计信息(min/max/null count)
  • 文件大小、行数

为什么要有清单这一层?直接让快照指向数据文件不行吗?

你想想看,如果一张表有 10 万个数据文件,快照直接引用它们,那快照文件本身就会变得巨大。Iceberg 的做法是:把数据文件分组,每组对应一个清单。快照只引用几十个清单文件,查询时再通过清单找到具体的数据文件。

这就像图书馆的索引系统:

  • 快照 = 图书目录(告诉你有哪些书架)
  • 清单 = 书架上的标签(告诉你这个书架上有哪些书)
  • 数据文件 = 具体的书

2.3.4 数据文件(Data File)

数据文件是真正存储行数据的物理文件。Iceberg 支持 Parquet、ORC、Avro 三种格式,我个人最常用 Parquet,因为它的列式存储和压缩比在分析场景下表现最好。

数据文件有几个关键特性:

  • 不可变性:一旦写入就不会被修改。更新操作是通过“删除旧文件 + 写入新文件”实现的。
  • 自描述:文件内嵌了 Schema 和统计信息,即使元数据丢失也能部分恢复。
  • 分区无关:数据文件不感知分区,分区信息只存在于清单中。这意味着你可以随意移动文件而不破坏分区逻辑。

一个重要的设计原则: Iceberg 的数据文件是“一次写入,多次读取”的。这种设计让 Iceberg 天然支持并发写入,因为不同写入者不会修改同一个文件。

2.4 四者如何协同工作?

咱们用一个实际查询的例子来串一遍:

  1. 客户端发起 SELECT * FROM orders WHERE order_date = '2025-01-15'
  2. Iceberg 从表的元数据中找到 当前快照 的指针
  3. 加载当前快照,获取 清单列表 的路径
  4. 读取清单列表,得到所有 清单文件 的路径
  5. 扫描每个清单文件,利用清单中的 分区统计信息 过滤掉不相关的分区
  6. 对于命中的分区,读取清单中记录的 数据文件路径 和 列统计信息
  7. 利用列统计信息(min/max)进一步过滤数据文件,只读取可能包含 order_date = '2025-01-15' 的文件
  8. 最终只扫描 3 个数据文件,而不是全表扫描

你看,整个过程就像漏斗一样,层层过滤。这就是 Iceberg 查询性能优于 Hive 的原因之一——元数据本身就能做剪枝。

2.5 一张表总结

概念 存储位置 核心内容 类比
表 metadata 目录 Schema、分区策略、快照指针 账本封面
快照 metadata 目录 清单列表路径、时间戳、操作摘要 账本的一页
清单 metadata 目录 数据文件路径、分区信息、列统计 书架标签
数据文件 数据目录(HDFS/S3) 实际的行数据 书的内容

嗯,这一章的内容就到这儿。这四个概念的关系理清了,后面讲分区演进、时间旅行、并发控制时,你就能快速理解 Iceberg 为什么能做出那些“反直觉”的设计。