表不是目录而是文件清单 — 从 metadata.json 到数据文件
一句话总结
Iceberg 表不是通过扫描目录来确定的,而是由 metadata.json → 清单列表 → 清单 → 数据文件这样逐层连接的列表树来定义的,提交就是先写好一棵新树,再原子地改掉 Catalog 中的一个指针。
为什么目录方式行不通
在 Hive 风格的表里,“表”就是目录。orders/order_date=2026-03-01/ 下面的文件就是那一天的数据,读取的引擎通过列出目录来收集文件。这种方式很简单,但有三个问题。
第一,没有原子性。写入作业在十个文件中只上传了五个的时候,如果读取作业列出目录,就会看到残缺的结果。第二,列出的代价很高。正如 Reliability 文档所指出的,Hive 表通过元存储(分区)和文件系统(文件)两个地方来跟踪状态,所以每次规划作业,都需要做与分区数成正比的列表查询。在对象存储上,这种查询很慢,过去甚至结果还会延迟才达到一致。第三,对什么是表没有共识。如果有人失误往目录里扔了一个文件,它也会成为表的一部分。
工作原理——四层的树
规范的概述在第一句话里就转换了方向:这种格式不是跟踪目录,而是逐个跟踪每一个文件。写入方可以把文件写在任何地方,只有通过显式的提交,才会把它加入表。
| 层 | 格式 | 内容 |
|---|---|---|
| metadata.json | JSON | schema 列表、分区规范列表、属性、快照列表、当前快照 ID、refs |
| 清单列表 | Avro | 构成一个快照的各个清单,以及各自的分区摘要和文件数 |
| 清单 | Avro | 每个数据(或删除)文件一行:路径、分区值、行数、各列的下界和上界 |
| 数据文件 | Parquet 等 | 实际的行 |
快照拥有 snapshot-id、指向父快照的 parent-snapshot-id、表示提交顺序的 sequence-number,以及自己的 manifest-list 路径。摘要(summary)中的 operation 是 append、replace、overwrite、delete 四者之一,同时还会记录新增的行数、文件数之类的数字。快照的数据是它的各个清单中所记录的存活文件的并集。
清单的一行(条目)带有 status——0 是 EXISTING,1 是 ADDED,2 是 DELETED。扫描规划不会使用 DELETED 的条目。而且它分两个阶段跳过内容:利用清单列表的分区摘要,整个清单被跳过;利用清单的列统计信息,文件被跳过。哪里都没有列出目录的操作。
提交做了什么
想一想第二次提交。写入新的数据文件,写入包含这一个文件的新清单,写入同时指向第一次提交的清单和新清单的新清单列表,再写入增加了一个快照的新 metadata.json。规范指出,清单会在快照之间被复用——旧的清单不会被改写,只是被指向。所以提交的开销与变更的量成正比,而不是与表的大小成正比。
最后,把 Catalog 中“当前 metadata 就是这个文件”的指针原子地改掉。此后打开表的人看到新状态,此前打开的人则始终看到旧状态。不存在残缺的状态。序列号每次提交增加一,在提交竞争中落败后重新提交时,只需重写清单列表即可,这是设计好的。
在现场相遇的样子
昨天放进去的数据看不到了。文件明明在存储桶里,却看不到行,几乎总是因为那个文件不在当前快照的清单里——写入作业只写了文件,在提交之前就挂掉了。在 Iceberg 中,这是正常行为。没有提交的文件不属于表。
目录删掉了,表却完好无损,或者正相反。数据文件的路径在清单中以完整路径记录。目录结构只是一种惯例,没有实际意义。手动删除文件会让表损坏(清单指向了不存在的文件),手动放入文件则什么都不会发生。
规划很慢。用流式写入每分钟提交一次的表,清单会达到几千个。要知道查询规划变慢的原因可能不在数据,而在元数据层,这样才会联系到文件合并和清单整理。
实际工作中真正重要的事
- 表的状态由一个 metadata.json 决定。它没有指向的东西,就不是表。
- 提交就是写一棵新树并改掉指针。旧树原样保留,成为时间旅行的素材。
- 清单会被复用。亲眼确认第二个快照原样指向第一个清单,对后面的文件合并和过期就容易理解了。
- 读取通过列表和统计信息来跳过。没有列出目录的操作,所以即使分区很多,规划的成本也不会爆炸。
下一项实验要做什么
用 Spark 创建表,把一天的订单分两次提交。然后不借助工具,顺着这棵树往下走:从 SQLite Catalog 的一行中读取当前和之前的 metadata 路径,用 jq 取出 metadata.json 中的快照列表(父快照、序列号、清单列表),解开 Avro 格式的清单列表,确认第二个快照原样指向第一次提交的清单,再解开清单,写下存活的数据文件和行数。评分器会把你写下的值与实际的元数据对照。