Skip to content

S1 第 3 期:主键表 vs Append 表,插两条一样的数据,结果天差地别 ​

Apache Paimon 源码学习

作者 X老师(DaemonforY),Paimon 2.0 / master 源码,按 CC BY-NC-SA 4.0 发布。配套实验和代码在 GitHub。

Yui和Kai对比主键表去重与追加表保留重复数据
Yui和Kai对比主键表去重与追加表保留重复数据AI 生成配图

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:去重语义 ​

主键表合并重复订单,Append表保留每次写入
主键表合并重复订单,Append表保留每次写入AI 生成配图
  • 主键表:同一主键只保留一条。默认合并引擎是 deduplicate(保留最新),也可以选 partial-update(部分列更新)、aggregation(按列聚合)、first-row(保留第一条)。
  • Append 表:没有“同一条”的概念,每次写入都追加。

区别 2:更新是怎么“做到”的——看物理记录数 ​

LSM树保留旧版本并追加新版本和删除标记
LSM树保留旧版本并追加新版本和删除标记AI 生成配图

查快照系统表 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 怎么知道哪个是最新的?

代码示例在页面里运行时使用 HiveGPT 的模型接口。延伸阅读来自 JavaGuide(Apache-2.0),版权归原作者。