S1 第 3 期:主键表 vs Append 表,插两条一样的数据,结果天差地别
Apache Paimon 源码学习
作者 X老师(DaemonforY),Paimon 2.0 / master 源码,按 CC BY-NC-SA 4.0 发布。配套实验和代码在 GitHub。

TL;DR:Paimon 有两种表。主键表按主键去重,同一主键只保留(合并后的)一条,底层是 LSM 树,读时要按主键归并;Append 表没有主键,写什么存什么,读时直接读文件。订单、用户、维表、CDC 同步用主键表;日志、埋点、事件流用 Append 表。
实验:两张表,同样的“重复写入”
主键表 orders(主键 dt, order_id,2 个桶),Append 表 access_log(无主键,不配 bucket)。
主键表:订单 1 已存在(CREATED),再写一条 (1, 101, 99.90, 'PAID', '2026-09-24')(同一条 INSERT 里还新增了订单 6;随后又删除了订单 3,所以结果里没有 3)。查询结果(节选,省略 SQL 与结果之间的日志):
Flink SQL> SELECT * FROM orders ORDER BY order_id;
+----------------------+----------------------+--------------+--------------------------------+--------------------------------+
| order_id | user_id | amount | status | dt |
+----------------------+----------------------+--------------+--------------------------------+--------------------------------+
| 1 | 101 | 99.90 | PAID | 2026-09-24 |
| 2 | 102 | 15.00 | CREATED | 2026-09-24 |
| 4 | 101 | 8.80 | CREATED | 2026-09-25 |
| 5 | 104 | 66.60 | CREATED | 2026-09-25 |
| 6 | 105 | 12.30 | CREATED | 2026-09-25 |
+----------------------+----------------------+--------------+--------------------------------+--------------------------------+
5 rows in set订单 1 被更新为 PAID,没有出现两条。
Append 表:再写一条和第一条完全相同的 (101, '/home', TIMESTAMP '2026-09-25 10:00:00')。查询结果(节选):
Flink SQL> SELECT * FROM access_log;
+----------------------+--------------------------------+-------------------------+
| user_id | url | ts |
+----------------------+--------------------------------+-------------------------+
| 101 | /home | 2026-09-25 10:00:00.000 |
| 102 | /item/1 | 2026-09-25 10:00:05.000 |
| 101 | /cart | 2026-09-25 10:01:00.000 |
| 101 | /home | 2026-09-25 10:00:00.000 |
+----------------------+--------------------------------+-------------------------+
4 rows in set第 1 行和第 4 行完全相同,两条都在。
区别 1:去重语义

- 主键表:同一主键只保留一条。默认合并引擎是
deduplicate(保留最新),也可以选partial-update(部分列更新)、aggregation(按列聚合)、first-row(保留第一条)。 - Append 表:没有“同一条”的概念,每次写入都追加。
区别 2:更新是怎么“做到”的——看物理记录数

查快照系统表 orders$snapshots(节选,省略 SQL 与结果之间的日志;manifest list 名每次运行都不同):
Flink SQL> SELECT snapshot_id, commit_kind, commit_identifier, total_record_count, delta_record_count,
> base_manifest_list, delta_manifest_list
> FROM `orders$snapshots`;
+----------------------+--------------------------------+----------------------+----------------------+----------------------+--------------------------------+--------------------------------+
| snapshot_id | commit_kind | commit_identifier | total_record_count | delta_record_count | base_manifest_list | delta_manifest_list |
+----------------------+--------------------------------+----------------------+----------------------+----------------------+--------------------------------+--------------------------------+
| 1 | APPEND | 9223372036854775807 | 5 | 5 | manifest-list-9565fe2b-61a5... | manifest-list-9565fe2b-61a5... |
| 2 | APPEND | 9223372036854775807 | 7 | 2 | manifest-list-e743f264-74bd... | manifest-list-e743f264-74bd... |
| 3 | APPEND | 9223372036854775807 | 8 | 1 | manifest-list-4ba37533-cc89... | manifest-list-4ba37533-cc89... |
+----------------------+--------------------------------+----------------------+----------------------+----------------------+--------------------------------+--------------------------------+
3 rows in set(快照 2 = 更新订单 1 + 新增订单 6;快照 3 = 删除订单 3)
查询只返回 5 行,但物理记录有 8 条。因为主键表底层是 LSM 树:更新不改旧文件,而是追加一条新版本(sequence number 更大);删除追加一条删除标记。查询时按主键把多个版本归并,取最新的那条。orders$files 里能看到:订单 1 的旧版本在 sequence 0~1 的文件里,新版本在 sequence 2 的文件里,所有文件都在 level 0(还没合并)。
Append 表没有这个过程,读的时候直接读文件、不需要归并(源码里两者分别走 MergeFileSplitRead 和 RawFileSplitRead)。
区别 3:分桶
- 主键表必须决定数据进哪个桶,因为同一主键必须落在同一个桶才能去重。可以固定桶数(本例
'bucket'='2'),默认bucket = -1是动态桶模式。 - Append 表默认
bucket = -1是 unaware-bucket 模式:数据都写在bucket-0目录下(实验中access_log/bucket-0/),读写并行度不受桶数限制。
区别 4:流式读取时看到的变更
- Append 表流读只有
+I(新增)。 - 主键表流读能看到
+I / -U / +U / -D完整的变更流(具体怎么产生和changelog-producer有关,后面专门讲)。
怎么选
| 场景 | 选择 |
|---|---|
| 订单、用户、库存等需要更新的实体数据 | 主键表 |
| MySQL 等数据库的 CDC 同步 | 主键表 |
| 维表(需要按主键 lookup) | 主键表 |
| 日志、埋点、监控指标等只增不改的数据 | Append 表 |
| 需要保留每一条原始事件 | Append 表 |
一句话:数据会被更新、需要按主键得到“最新状态”,选主键表;数据只追加、每条都要保留,选 Append 表。
下期预告:既然更新是追加新版本,那删除呢?删除一行数据,Paimon 反而多写了一个文件。
自测题:主键表里,订单 1 的两个版本分别在哪个文件、sequence number 是多少?读取时 Paimon 怎么知道哪个是最新的?