读完 列存引擎内核(ClickHouse / DuckDB 读优化)、PostgreSQL 行存 OLTP 与 LSM 写优化引擎,你已经熟悉「数据在一个数据库进程里、由它统一管理页、索引、事务」的世界。lakehouse 换了一个前提:数据不在任何进程里,而是一堆文件躺在对象存储上,没有常驻的存储引擎进程为它们维护一致性。 那么「一张支持 ACID、能更新删除、能时间旅行的表」是怎么在这种底座上成立的?
这正是本系列要拆的内核问题。第一篇不展开任何单一格式,而是回答三个更基础的问题:
- 在开放表格式出现之前,人们怎么把对象存储上的文件当表用?这套做法(Hive 目录式分区表)到底卡在哪?
- lakehouse 把表重新拆成了哪几层,每一层解决 Hive 的哪个硬伤?
- 「data lake / data warehouse / lakehouse」这三个被市场用滥的词,技术边界到底在哪?
本篇是全景与地图,不依赖本机实测;涉及具体格式与提交协议的实测留到后续各章。结论锚定一手来源:Apache Iceberg 表规范与官方文档对 Hive 表问题的陈述、Apache Hive 的 Metastore 文档、以及 lakehouse 概念的原始论文(Armbrust 等,CIDR 2021)与 Delta Lake 论文(Armbrust 等,VLDB 2020)。
一、先把「表」这个词拆开
在数据库里,「表」是一个被引擎牢牢握住的对象:引擎知道它有哪些行、存在哪些页、被哪些索引覆盖、当前有哪些未提交事务。读写都经过引擎,一致性由引擎的 MVCC、锁、WAL 保证(见 PostgreSQL MVCC)。
把数据搬到对象存储上之后,这个「握住表的引擎」消失了。剩下的只有:
- 一个对象存储桶里的一堆文件(通常是 Parquet / ORC)。
- 某个地方的一点元信息,告诉你「这张表由哪些文件组成、schema 是什么」。
「表格式(table format)」要回答的就是后面这半句:给定一个表名,当前这一刻它由哪些文件组成、用什么 schema 解释、能不能并发安全地往里加减文件。 这件事在有引擎时被隐含解决了,在对象存储上必须显式设计。Hive 给出了第一代答案,开放表格式(Iceberg / Delta / Hudi)给出了第二代。要讲清第二代为什么长成那样,得先看清第一代卡在哪。
文件格式 ≠ 表格式
一个高频混淆先澄清:Parquet 是文件格式,Iceberg 是表格式,两者不在一个层次。
- 文件格式(Parquet、ORC、Avro)管「一个文件内部如何编码、压缩、组织成列」,回答「单个文件怎么读」。它是本系列第 1 部分(第 2–5 章)的主题。
- 表格式(Iceberg、Delta、Hudi)管「许多文件如何组成一张有事务语义的表」,回答「这张表此刻由哪些文件组成、怎么原子地改变这个集合」。它是第 3 部分起(第 8 章之后)的主题。
一张 Iceberg 表里的数据文件几乎总是 Parquet(也可以是 ORC/Avro)。所以本系列的顺序是先讲文件格式(第 2–5 章),再讲对象存储语义(第 6 章),最后才进表格式(第 7 章起)。本篇把这条链先铺出来。
flowchart TD
Q["查询引擎<br/>Trino / Spark / DuckDB"]
CAT["catalog 层<br/>表名 → 当前元数据指针"]
TF["表格式层<br/>snapshot / 事务日志 / timeline"]
OBJ["对象存储层<br/>S3 语义:不可变对象 + 条件写"]
FF["文件格式层<br/>Parquet / ORC 内部结构"]
Q --> CAT
CAT --> TF
TF --> OBJ
TF --> FF
FF --> OBJ
这张图是全系列的地图:自底向上是「文件怎么读 → 对象存储能给什么 → 表格式如何在其上建事务 → catalog 如何定位与原子提交 → 引擎如何下推查询」。本篇把每一层的存在理由讲清,后续各章逐层钻进去。
二、第一代答案:Hive 目录式分区表
Hadoop 时代,「在 HDFS / 对象存储上把文件当表查」的事实标准是 Hive 表:用 Hive Metastore(HMS)存表与分区的元数据,用目录结构表达分区,用目录里的文件作为数据。今天绝大多数「老数据湖」仍是这个模型,理解它是理解开放表格式的前提。
2.1 它长什么样
一张按日期分区的 Hive 表,物理上是这样一棵目录树(对象存储上则是共享前缀的 key,见 对象存储语义与代价):
s3://warehouse/db/orders/
date=2026-06-29/
part-00000.parquet
part-00001.parquet
date=2026-06-30/
part-00000.parquet
part-00002.parquet
元数据侧,Hive Metastore 里存着(来源:Apache Hive
文档,AdminManual Metastore 与
Table/Partition 模型):
- 表级:表名、schema(列名与类型)、表存储位置(
s3://warehouse/db/orders/)、分区键(date)、表属性。 - 分区级:每个分区一行,记录分区值(
date=2026-06-30)与该分区的存储位置(一个目录前缀)。
注意一个关键事实:Metastore
跟踪到「分区」这一级就停了,它不记录分区目录里有哪些文件。
「这个分区由哪些文件组成」要靠运行时去 LIST
那个目录前缀得到。这一条是后面所有麻烦的根。
flowchart LR
HMS["Hive Metastore<br/>表 schema + 分区行(目录前缀)"]
D1["date=2026-06-29/ (目录)"]
D2["date=2026-06-30/ (目录)"]
HMS -->|"分区行指向前缀"| D1
HMS -->|"分区行指向前缀"| D2
D1 -.->|"运行时 LIST 才知道有哪些文件"| F1["part-*.parquet"]
D2 -.->|"运行时 LIST 才知道有哪些文件"| F2["part-*.parquet"]
2.2 一次查询怎么跑
查询
SELECT sum(amount) FROM orders WHERE date = '2026-06-30'
在 Hive 模型下的 planning 大致是:
- 问 Metastore:表
orders的位置、schema、分区键。 - 问 Metastore:满足
date = '2026-06-30'的分区有哪些(分区裁剪在这一步,靠分区行)。 - 对命中的分区目录做
LIST,枚举里面的数据文件。 - 打开这些 Parquet 文件读取(文件内部还能按 row group / page 再裁,见 Parquet 文件格式深拆)。
第 2 步的分区裁剪是 Hive
模型唯一像样的优化:不读不相关日期的目录。但第 3
步是命门——要知道一个分区里有哪些文件,只能
LIST 目录。 在 HDFS 上
LIST 还算便宜,搬到 S3
这种对象存储上,LIST 是按前缀分页的网络
API,代价随对象数线性增长(见 对象存储语义与代价
第三节)。这是「小文件多了 planning 就慢」的直接来源。
三、Hive 表的三个硬伤
把 Hive 模型的问题归纳成三条,每一条都直接对应开放表格式后来补上的一件事。以下问题陈述锚定 Apache Iceberg 官方文档对「Hive 表的问题」的总结(Iceberg Documentation 与表规范 Overview 对设计动机的陈述)。
3.1 硬伤一:没有原子提交,依赖目录 rename
写入一张 Hive 表(比如一次 INSERT OVERWRITE
或追加一批文件),经典做法是 Hadoop 的
FileOutputCommitter:先把文件写到一个临时目录
_temporary/,全部写完后
rename 到最终目录。在 HDFS /
POSIX 上,目录 rename 是原子且 O(1) 的,于是「rename
成功」≈「这批数据原子地出现了」。
这套机制在对象存储上塌掉,因为对象存储没有原子 rename(详见 对象存储语义与代价 第四节):
- 「重命名目录」退化成「对前缀下每个对象
CopyObject+DeleteObject」,是 O(对象数) 的搬字节操作。 - 这个过程不是原子的:copy 到一半失败,目标前缀里只有半批文件;此刻有 reader 来扫,就读到一张「写了一半」的表(partial commit)。
- 多个 writer 并发往同一张表写,没有任何机制仲裁谁赢,可能互相覆盖或交错。
结果是:Hive 表在对象存储上既给不了「全有或全无」的原子提交,也给不了并发写的隔离。 工程上只能靠「同一时刻只允许一个写任务」「写完用业务约定切换分区」等外部纪律兜,脆弱且不通用。这是第 7 章「表格式为什么存在」要正面回答的问题。
3.2 硬伤二:planning 依赖 LIST,随规模线性退化
如 2.2 所述,Hive 要知道分区里有哪些文件,只能
LIST 目录。这带来两个层面的问题:
- 性能:
LIST在对象存储上是 O(前缀下对象数) 的分页网络往返(S3 单页最多 1000 个 key,见第 6 章)。一张几十万小文件的表,光「列出有哪些文件」就要成百上千次往返。深层分区(year/month/day/hour)还要逐层LIST,往返更多。 - 正确性历史包袱:S3 在 2020
年底才提供强一致(见第 6 章第二节),在那之前
LIST可能列不全刚写的对象,导致查询漏读。虽然这个具体问题随强一致已消失,但「planning 的正确性依赖存储的列举语义」这个耦合本身就是脆弱设计。
Hive
模型里没有文件级的统计信息——它不知道某个文件里
amount
的取值范围,所以无法在打开文件前按文件裁剪。能裁的只有分区目录这一粒度。要进一步裁,只能打开每个
Parquet 文件读 footer 里的统计(又是一次次往返)。
开放表格式的对策是把「表由哪些文件组成」和「每个文件的列级统计」预先写进元数据文件,planning
时读固定几个元数据对象,而不是 LIST
数据目录、也不必逐个打开数据文件。这是第 8 章(Iceberg
元数据树)的核心。
3.3 硬伤三:schema 与分区演进笨重
Hive 表的两个演进点都别扭:
- schema 演进靠列的位置/名字。Hive 表的列与 Parquet 文件里的列默认靠名字或位置对应。改列名、调列序、删中间一列,很容易让老文件与新 schema 对不上,轻则读出错值,重则要重写数据。Hive 在不同存储格式下对列解析的行为并不统一,是经典踩坑点。
- 分区是物理目录,改不动。分区键编码在目录路径里(
date=.../)。一旦想把按天分区改成按小时,或想换一个分区列,老数据的目录结构对不上新方案,通常只能重写整张表的数据布局。而且查询必须显式在分区列上写谓词(WHERE date = ...)才能裁剪——用户写WHERE ts >= ...而忘了写date,就会全表扫描(「分区裂脑」)。
开放表格式用两招破这两点(细节在第 9、16 章):
- 列用全局唯一的 field id 标识,而不是名字或位置。schema 演进只改 id↔︎名字的映射,老文件按它当年的 id 解释,不用重写。
- 分区是元数据里的 partition spec(带
transform),不是物理目录。
day(ts)这样的隐藏分区让用户写ts >= ...也能自动裁剪;分区方案演进只新增一个 spec,老文件继续按旧 spec 解释,不重写历史数据。
三个硬伤与解法对照
| Hive 硬伤 | 根因 | 开放表格式的解法 | 本系列章节 |
|---|---|---|---|
| 无原子提交、依赖目录 rename | 对象存储无原子 rename,commit-by-rename 失效 | 提交 = 原子切换一个元数据指针(CAS) | 第 7、11 章 |
| planning 靠 LIST,无文件级 stats | Metastore 只跟踪到分区,文件靠运行时枚举 | 元数据预存文件清单 + 列级统计,不 LIST | 第 8 章 |
| schema/分区演进笨重 | 列靠名字/位置、分区是物理目录 | field id 对齐 + partition spec 演进,不重写 | 第 9、16 章 |
这张表是全系列的「问题驱动」索引:每一行的右侧解法都对应后面专门的一章。
四、第二代答案:把表拆成三层
开放表格式(Iceberg / Delta / Hudi)的共同抽象,可以概括成一句话:一张表 = 一堆不可变的数据文件 + 一份可变的元数据 + 一个能原子切换元数据的 catalog。 三家在「元数据怎么组织」上各有取法(snapshot 树 / 事务日志 / timeline,第 8、12、13 章对比),但分层是共通的。
flowchart TD
C["catalog 指针<br/>表名 → 当前元数据位置 (唯一可变量)"]
M["元数据<br/>记录: 当前由哪些文件组成 + 每文件的列级统计 + schema/分区历史"]
D["数据文件 (不可变)<br/>Parquet / ORC,写完不再改"]
C -->|"提交 = 原子 swap"| M
M -->|"引用 + 统计"| D
4.1 第一层:不可变数据文件
数据落成一个个 Parquet / ORC 文件,写完就不再修改。这不是风格选择,而是被对象存储逼出来的:对象不可局部改写(见第 6 章第六节),想「改」只能写新文件。
不可变带来一串连锁结论:
- 行级 update/delete 不能原地改文件。要么重写包含目标行的整个文件(copy-on-write),要么写一个独立的 delete 文件、读时合并(merge-on-read)。这是第 10 章的主题。
- 时间旅行几乎免费。旧文件不被改写、不被立刻删除,按旧的元数据指针就能读到历史版本(第 16 章)。
- 统计可以放心回填进元数据。文件不变,它的 min/max/null count 等统计就永远有效,写进元数据后不会失真(除非文件被替换)。
4.2 第二层:可变元数据
元数据是表格式的大脑,至少记录三件事:
- 当前表由哪些数据文件组成——把 Hive 要靠
LIST才知道的事,变成读元数据就知道。 - 每个数据文件的列级统计(min/max、null count 等)——把 Hive 没有的文件级裁剪能力补上。
- schema 与分区的历史与当前指针——用 field id 与 partition spec 支撑无重写演进。
关键在于元数据本身也大多是不可变文件 + 写新换旧:每次提交生成新的元数据文件,旧的不动。这样「读一个历史版本」就是「读一份旧元数据」。Iceberg 把它组织成一棵四层树(metadata.json → manifest list → manifest → data file,第 8 章),Delta 组织成一串有序事务日志(第 12 章),Hudi 组织成 timeline(第 13 章)。
4.3 第三层:catalog 与原子提交
三层里唯一被原地改写的,是 catalog 里那一个「表名 → 当前元数据位置」的指针。提交一次写入的最后一步,就是把这个指针从「旧元数据」原子地切到「新元数据」。
- 切换成功 = 新写的全部文件同时变可见(原子提交)。
- 切换基于「当前指针仍是我读到的那个版本」做 compare-and-swap,两个并发 writer 只有一个能切成功,另一个失败后基于新状态重试(乐观并发,第 11 章)。
这个「原子 swap」从哪来?取决于 catalog 的实现(第 15
章详谈):数据库 catalog 用一行的条件更新(行锁/事务),REST
catalog
用后端服务的事务,纯对象存储则可用条件写(If-Match/If-None-Match,第
6 章第五节)。把「提交一张表」从 Hive 的「搬 N
个文件并祈祷 rename 原子」压缩成「换 1
个指针」,是开放表格式最核心的一招。
sequenceDiagram
participant W as Writer
participant S as 对象存储
participant C as Catalog
W->>S: 写新数据文件 (Parquet, 不可变)
W->>S: 写新元数据 (引用新+旧文件, 含统计)
W->>C: CAS: 指针 旧元数据 → 新元数据
alt 指针未变
C-->>W: 成功 (新文件原子可见)
else 被别人抢先
C-->>W: 冲突, 读新状态后重试
end
对照第 3 章的三个硬伤:原子 swap 解决了「无原子提交」;元数据预存文件清单与统计解决了「靠 LIST、无文件级裁剪」;field id 与 partition spec 解决了「演进笨重」。三层抽象不是凭空设计,而是逐一回应 Hive 的痛点。
五、三个被用滥的词:data lake / warehouse / lakehouse
讲清了技术分层,再来界定三个市场词的技术边界。这里不做营销式定义,只按「数据放哪、谁管 schema、有没有事务」三个维度区分。
| 维度 | 数据仓库 (data warehouse) | 数据湖 (data lake) | 湖仓 (lakehouse) |
|---|---|---|---|
| 数据位置 | 仓库自有存储(常为专有格式) | 对象存储 / HDFS 上的开放格式文件 | 对象存储上的开放格式文件 |
| schema | 写时 schema(schema-on-write) | 读时 schema(schema-on-read) | 写时 schema(表格式强制) |
| 事务 / ACID | 有 | 基本没有(Hive 表只能弱保证) | 有(表格式提供) |
| 引擎耦合 | 强(数据进仓库才能查) | 弱(任意引擎读文件) | 弱(多引擎共享表格式) |
| 典型问题 | 贵、封闭、难放半结构化数据 | 没有事务、易成「数据沼泽」 | 复杂度上移到表格式与 catalog |
三者的演化逻辑:
- 数据仓库把数据收进自有系统,强 schema、强事务,但封闭、扩展贵、对原始/半结构化数据不友好。
- 数据湖反过来:把任意格式的原始数据廉价堆在对象存储上,任意引擎来读。代价是丢了事务与治理——Hive 表那三个硬伤就是「数据湖没有事务层」的具体表现。没有治理的数据湖容易退化成没人敢信的「数据沼泽」。
- 湖仓的主张(Armbrust 等,Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics,CIDR 2021)是:在数据湖的开放存储之上,加一层有事务语义的元数据(表格式),让对象存储上的文件也能像仓库表一样支持 ACID、更新、时间旅行,同时保留「开放格式 + 多引擎 + 廉价对象存储」的好处。
所以 lakehouse 在技术上 = 数据湖的存储底座 + 仓库级的表语义,而把仓库语义落到对象存储上的那层东西,就是本系列要拆的开放表格式。Delta Lake 论文(Armbrust 等,Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores,VLDB 2020)给出了这一思路最早的完整工程实现之一:用一份事务日志在 S3 上做 ACID。
一句话边界:lakehouse 不是某个产品,而是「开放文件格式 + 表格式元数据 + catalog + 多引擎」这套组合。本系列拆的是这套组合的内核,不是任何厂商的托管实现。
六、为什么 Iceberg 作主线
三家开放表格式都遵循第四节的三层抽象,本系列选 Apache Iceberg 作主线,Delta / Hudi 各一章对照(第 12、13 章),第 14 章做三者对比与互通。选择理由(不做排名,只说本系列的取法):
| 维度 | Iceberg | Delta Lake | Hudi |
|---|---|---|---|
| 元数据模型 | 不可变 snapshot + manifest 树 | 顺序事务日志 _delta_log |
timeline + file group |
| 规范开放度 | 表规范 + REST Catalog 规范公开 | 协议公开,生态偏 Spark/Databricks | 协议公开,生态偏流式 upsert |
| 引擎中立性 | 强(Trino/Spark/Flink/DuckDB 等) | 较强(UniForm 补互通) | 偏自带写入栈 |
| 行级更新 | position/equality delete、V3 deletion vector | deletion vector | CoW/MoR + record index |
选 Iceberg 主要因为:表规范(V1/V2/V3)与 REST Catalog 规范完整公开、便于锚定一手来源;引擎中立性最强,适合讲「表格式与引擎解耦」这件事。这不代表 Iceberg「更好」——三家是同一问题(在对象存储上给文件加事务)的不同解法,各自的取舍在第 14 章展开。
版本锚定:本系列以 Iceberg 表规范 V2 为主线,V3 新特性(deletion vector、row lineage 等)单独标注「规范已定义、引擎支持视版本而定」;库版本 Iceberg 1.x、Delta 3.x+、Hudi 1.x,源码与规范引用都标注 release / spec 版本。
七、本系列的分层地图与阅读路径
把全系列 21 篇按第一节的分层地图归位,每一章在解决哪一层的问题:
flowchart TD
subgraph L1["文件格式层 (第 2-5 章)"]
P["02 Parquet"] --> O["03 ORC 对照"]
P --> A["04 Arrow 内存格式"]
A --> E["05 编码与压缩"]
end
subgraph L2["对象存储与动机 (第 6-7 章)"]
S["06 对象存储语义"] --> WT["07 表格式为何存在"]
end
subgraph L3["表格式内核 (第 8-14 章)"]
IM["08 Iceberg 元数据树"] --> HP["09 隐藏分区"]
IM --> RD["10 行级删除/MoR"]
IM --> CC["11 提交与并发"]
DL["12 Delta"] --> CMP["14 三者对照"]
HU["13 Hudi"] --> CMP
RD --> CMP
end
subgraph L4["治理/引擎/前沿 (第 15-21 章)"]
CATALOG["15 Catalog 之争"]
QE["18 引擎如何读湖"]
end
L1 --> L2 --> L3 --> L4
几条推荐路径(完整路径见 系列目录):
- 从列存来的读者:先看 Parquet 文件格式深拆 → ORC 对照 → 第 8 章,把已有的列存直觉接到表格式上。
- 数据平台工程师:本篇 → 第 2 章 → 第 8 章 → 小文件与 compaction(第 17 章)→ 引擎读湖(第 18 章)→ 运维(第 20 章)。
- 表格式选型:本篇 → 第 8 章 → Delta(第 12 章)→ Hudi(第 13 章)→ 对照(第 14 章)→ catalog(第 15 章)。
无论走哪条路径,下一站都建议是 Parquet 文件格式深拆:表格式管的是「哪些文件组成表」,而「文件内部怎么读、谓词如何在读盘前裁页」是 Parquet 这层的事,也是整套裁剪漏斗(partition pruning → file pruning → page pruning)的最后一级。
八、小结
Hive 目录式分区表是「在对象存储上把文件当表用」的第一代答案,它把表等同于「一组目录 + Metastore 里的分区行」,于是栽在三处:
- 没有原子提交:依赖目录 rename,而对象存储没有原子 rename,导致 partial commit 与并发写无隔离。
- planning 靠 LIST:Metastore 只跟踪到分区,文件清单要运行时枚举;没有文件级统计,裁剪粒度只到分区目录。
- 演进笨重:列靠名字/位置、分区是物理目录,schema 与分区演进常要重写数据。
开放表格式(Iceberg / Delta / Hudi)的共同对策是把表拆成三层:不可变数据文件 + 可变元数据 + 可原子切换的 catalog 指针。提交从「搬 N 个文件」变成「换 1 个指针」(解决硬伤一),元数据预存文件清单与列级统计(解决硬伤二),field id 与 partition spec 支撑无重写演进(解决硬伤三)。这就是 lakehouse 在技术上的实质:数据湖的开放存储底座 + 仓库级的表语义。
下一篇进入文件格式层的核心:Parquet 文件格式深拆,看一个 Parquet 文件如何切成 row group / column chunk / page,谓词如何在读盘前裁掉数据。
返回 系列目录
下一篇:Parquet 文件格式深拆
参考资料
- Apache Iceberg, Apache Iceberg Documentation(iceberg.apache.org)与表规范 Overview——对 Hive 表问题(依赖目录 LIST、无文件级跟踪、原子性与演进)的设计动机陈述。
- Apache Hive, AdminManual Metastore 与 Hive
Table/Partition元数据模型文档——Metastore 跟踪到分区粒度、分区指向目录前缀。 - Apache Hadoop, Committing work to S3 with the S3A
Committers——经典
FileOutputCommitter依赖目录 rename 的原子性,对象存储不满足。 - M. Armbrust, A. Ghodsi, R. Xin, M. Zaharia, Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics, CIDR 2021——lakehouse 概念的原始论文。
- M. Armbrust 等, Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores, VLDB 2020——在对象存储上用事务日志做 ACID 的工程实现。
- 本系列 对象存储语义与代价——S3 无原子 rename、LIST 代价、条件写的一手语义。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据湖与开放表格式】Parquet · Iceberg · Delta · Hudi 内核拆解
拆解 lakehouse 的两层基础:列式文件格式(Parquet/ORC/Arrow)与开放表格式(Iceberg/Delta/Hudi)。讲清没有数据库进程时,如何在对象存储上做 ACID、行级更新、快照与并发,以及 catalog、查询引擎、流式入湖如何拼成可运维的湖仓。面向数据平台工程师与从 OLAP/数仓转型的开发者。
【数据湖与开放表格式】表格式为什么存在
目录式分区表(Hive 表)在对象存储上有三处硬伤:并发写部分提交、list planning 太贵、缺快照隔离与原子提交。本文拆开放表格式补上的四件事——原子提交、快照隔离、文件级统计裁剪、schema 与分区演进,并抽象出三家共有的『元数据指针 + 不可变数据文件』骨架。
【数据湖与开放表格式】对象存储语义与代价
对象存储不是网络版 POSIX 文件系统。本文用 S3 官方语义钉住四件事:强一致模型的边界、LIST 随对象数线性增长的代价、没有原子 rename(只能 copy+delete)、条件写(If-None-Match/If-Match)对提交协议的意义,并讲清 multipart 与对象不可改写。
【数据湖与开放表格式】Iceberg 元数据树
拆解 Iceberg 的四层元数据:catalog 指针 → metadata.json → manifest list(snapshot)→ manifest file → data file。讲清 snapshot 与 manifest 里的分区数据和列级 stats(lower/upper bound、null/value count)如何让一次查询不 list 目录就收敛到文件集合,并给出表规范 V1/V2/V3 的版本边界。基于 pyiceberg 0.11.1 真实建表逐层 dump。