2、核心概念速览:表、快照、清单、数据文件的关系图谱
好,咱们直接进入正题。Iceberg 的四个核心概念——表(Table)、快照(Snapshot)、清单(Manifest)、数据文件(Data File)——说白了就是一套层层递进的元数据索引体系。我刚开始接触时也觉得绕,但一旦理清它们之间的“父子关系”,后面所有操作都会变得很自然。
2.1 一句话概括各自职责
先给个速记版:
- 表:逻辑容器,你写 SQL 时操作的对象。它不存数据,只存元数据指针。
- 快照:表在某个时刻的完整视图。每次提交(INSERT/UPDATE/DELETE)都会生成一个新快照。
- 清单:快照的“目录文件”,记录了一组数据文件的元信息(列统计、行数、路径等)。
- 数据文件:真正存数据的 Parquet/ORC/Avro 文件,躺在 HDFS 或 S3 上。
嗯,这里要注意:快照不是数据拷贝,它只是一个指针列表。所以 Iceberg 做时间旅行时,不会像 Hive 那样把整个分区目录复制一遍。
2.2 它们是怎么串起来的?
我画了一张关系图,帮你直观理解这个层级结构。你看完应该就能明白为什么 Iceberg 的查询可以做到“无锁并发”。
核心关系链:
表 → 指向当前快照 → 快照指向一组清单 → 每个清单指向一组数据文件
我的经验: 刚上手时,可以把“快照”想象成 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 四者如何协同工作?
咱们用一个实际查询的例子来串一遍:
- 客户端发起
SELECT * FROM orders WHERE order_date = '2025-01-15' - Iceberg 从表的元数据中找到 当前快照 的指针
- 加载当前快照,获取 清单列表 的路径
- 读取清单列表,得到所有 清单文件 的路径
- 扫描每个清单文件,利用清单中的 分区统计信息 过滤掉不相关的分区
- 对于命中的分区,读取清单中记录的 数据文件路径 和 列统计信息
- 利用列统计信息(min/max)进一步过滤数据文件,只读取可能包含
order_date = '2025-01-15'的文件 - 最终只扫描 3 个数据文件,而不是全表扫描
你看,整个过程就像漏斗一样,层层过滤。这就是 Iceberg 查询性能优于 Hive 的原因之一——元数据本身就能做剪枝。
2.5 一张表总结
| 概念 | 存储位置 | 核心内容 | 类比 |
|---|---|---|---|
| 表 | metadata 目录 | Schema、分区策略、快照指针 | 账本封面 |
| 快照 | metadata 目录 | 清单列表路径、时间戳、操作摘要 | 账本的一页 |
| 清单 | metadata 目录 | 数据文件路径、分区信息、列统计 | 书架标签 |
| 数据文件 | 数据目录(HDFS/S3) | 实际的行数据 | 书的内容 |
嗯,这一章的内容就到这儿。这四个概念的关系理清了,后面讲分区演进、时间旅行、并发控制时,你就能快速理解 Iceberg 为什么能做出那些“反直觉”的设计。