S1 第 2 期:打开一张 Paimon 表的目录,里面都是什么
Apache Paimon 源码学习
作者 X老师(DaemonforY),Paimon 2.0 / master 源码,按 CC BY-NC-SA 4.0 发布。配套实验和代码在 GitHub。

TL;DR:一张 Paimon 表就是一个目录。数据在
分区/bucket-N/下的 parquet 文件里;**“哪些数据文件属于当前版本”**由snapshot → manifest list → manifest三层元数据描述。读数据时,Paimon 先找到最新的 snapshot,再顺着清单找到数据文件。
实验:建一张表,写 5 行
CREATE TABLE orders (
order_id BIGINT, user_id BIGINT, amount DECIMAL(10,2), status STRING, dt STRING,
PRIMARY KEY (dt, order_id) NOT ENFORCED
) PARTITIONED BY (dt) WITH ('bucket' = '2');
INSERT INTO orders VALUES
(1, 101, 99.90, 'CREATED', '2026-09-24'), (2, 102, 15.00, 'CREATED', '2026-09-24'),
(3, 103, 250.00, 'CREATED', '2026-09-24'), (4, 101, 8.80, 'CREATED', '2026-09-25'),
(5, 104, 66.60, 'CREATED', '2026-09-25');写完后,orders 表目录里一共 10 个文件(在表目录下用 find . -type f -exec ls -l {} \; | awk '{print $5, $9}' | sort -k2 列出,左列为字节数):
1979 ./dt=2026-09-24/bucket-0/data-76887e73-7107-4449-b106-dc461ac9be77-0.parquet
1827 ./dt=2026-09-24/bucket-1/data-b789be4f-f68d-455b-8be9-b2556462800d-0.parquet
1979 ./dt=2026-09-25/bucket-0/data-6ab8c9c6-2160-4639-ac31-a928934097d7-0.parquet
2270 ./manifest/manifest-832affb5-9651-4dbd-8d15-931e3d1ae4a0-0
1006 ./manifest/manifest-list-9565fe2b-61a5-47b9-afc4-df6cc1dcbab2-0
1132 ./manifest/manifest-list-9565fe2b-61a5-47b9-afc4-df6cc1dcbab2-1
587 ./schema/schema-0
1 ./snapshot/EARLIEST
1 ./snapshot/LATEST
595 ./snapshot/snapshot-1(文件名中的 UUID 每次运行都不同,字节数以 Paimon 2.0.0 为准。)
从下往上看。
1. 数据文件:分区/bucket-N/data-*.parquet

dt=2026-09-24是分区目录(PARTITIONED BY (dt))。bucket-0、bucket-1是桶:每个分区内按主键哈希分成 2 个桶('bucket' = '2')。桶是 Paimon 读写并行的最小单位,每个桶内部是一棵独立的 LSM 树。- 一个有意思的细节:
dt=2026-09-25只有bucket-0,没有bucket-1。因为这个分区的两个订单(4 和 5)哈希后都落在了 0 号桶,1 号桶没有数据,就不会创建目录。分桶公式是abs(主键哈希 % 桶数)。 - 这次写入一共产生了 3 个数据文件,日志里也印证了这一点:“number of commit messages: 3”——每个有数据的桶一条提交信息。
2. snapshot:一个版本的“入口”
snapshot/snapshot-1 是一个 JSON 文件,完整内容如下(下面只解释其中几个关键字段):
{
"version" : 3,
"uuid" : "fb1a82a0-0a23-4246-a139-d8535b3abffe",
"id" : 1,
"schemaId" : 0,
"baseManifestList" : "manifest-list-9565fe2b-61a5-47b9-afc4-df6cc1dcbab2-0",
"baseManifestListSize" : 1006,
"deltaManifestList" : "manifest-list-9565fe2b-61a5-47b9-afc4-df6cc1dcbab2-1",
"deltaManifestListSize" : 1132,
"commitUser" : "eda2c146-bb92-4071-8aa1-b47950558eec",
"commitIdentifier" : 9223372036854775807,
"commitKind" : "APPEND",
"timeMillis" : 1791118454246,
"totalRecordCount" : 5,
"deltaRecordCount" : 5,
"watermark" : -9223372036854775808,
"nextRowId" : 0
}id:版本号,每次提交 +1。schemaId:这个版本用的表结构,对应schema/schema-0。baseManifestList:上一个版本的全量文件清单。这是第一次提交,没有历史,所以它是个空清单(只有 1006 字节)。deltaManifestList:本次提交新增/删除了哪些文件。commitKind:APPEND 表示普通写入;以后还会看到 COMPACT(合并)、OVERWRITE(覆盖)。totalRecordCount/deltaRecordCount:总记录数、本次新增记录数。
一个快照 = 某一时刻“哪些数据文件有效”的完整清单。 读最新数据就是读最新快照,读历史数据(时间旅行)就是读旧快照。
3. manifest list 与 manifest:两级文件清单

- manifest list 是“清单的清单”:记录了若干个 manifest 文件的名字和统计信息。
- manifest 记录具体的数据文件条目:
ADD或DELETE+ 文件元数据(所在分区、桶、level、主键范围、行数、统计信息……)。 - 用系统表
orders$manifests可以看到:这个 manifest 里有 3 条 ADD(num_added_files = 3),正好对应 3 个数据文件。
为什么要两级?文件数量多了以后,每次提交只需要写新增部分的 manifest,旧的 manifest 可以复用,提交不会随表变大而变慢。
4. LATEST / EARLIEST:两个“提示”文件
LATEST内容是1,EARLIEST内容也是1:最新和最早的快照号。- 注意它们只是提示(hint)。源码里(
HintFileUtils.findLatest)读到 LATEST = N 后,还会检查snapshot-(N+1)是否存在;如果存在,说明提示过期了,就退回到列目录查找。所以就算这两个文件更新不及时,也不会读错版本。
5. schema:表结构的版本
schema/schema-0 记录了字段、分区键、主键和表参数(完整内容):
{
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "order_id",
"type" : "BIGINT NOT NULL"
}, {
"id" : 1,
"name" : "user_id",
"type" : "BIGINT"
}, {
"id" : 2,
"name" : "amount",
"type" : "DECIMAL(10, 2)"
}, {
"id" : 3,
"name" : "status",
"type" : "STRING"
}, {
"id" : 4,
"name" : "dt",
"type" : "STRING NOT NULL"
} ],
"highestFieldId" : 4,
"partitionKeys" : [ "dt" ],
"primaryKeys" : [ "dt", "order_id" ],
"options" : {
"bucket" : "2"
},
"comment" : "",
"timeMillis" : 1791118447925
}每个字段都有一个 id。以后修改表结构(加列、改名)会生成 schema-1,Paimon 靠字段 id 而不是名字来对应新旧文件里的列。
一个能直接证明这一点的实验(仓库实验 6,用的是另一张表 schema_demo (order_id, status, amount),字段 id 依次为 0、1、2):删掉 amount 列再加回一个同名的 amount 列,旧数据的 amount 全部变成 NULL——因为新列拿到的是新 id(4),旧文件里的数据属于 id 2,对不上了。改列名则相反:status 改名为 order_status 后 id 仍是 1,旧数据照常读出。
一张图串起来:读数据时发生了什么
LATEST(提示) ──► snapshot-1 ──► manifest-list ──► manifest ──► 3 个 parquet 文件
│
└──► schema-0(用哪个表结构解析)小结
| 文件 | 作用 |
|---|---|
分区/bucket-N/data-*.parquet | 真正的数据 |
manifest-* | 数据文件条目(ADD/DELETE + 元数据) |
manifest-list-* | manifest 的清单 |
snapshot-N | 一个版本的入口:指向 base/delta 清单 |
LATEST / EARLIEST | 最新/最早快照号的提示 |
schema-N | 表结构版本 |
下期预告:同样往表里插两条一模一样的数据,主键表和 Append 表的结果天差地别。
自测题:如果手动删掉 snapshot/LATEST,这张表还能正常读吗?为什么?