Apache Paimon Partial-Update
总结
- 更新时间:2025-12-12
- 本文档详细介绍 Paimon 的 partial-update 合并引擎和 sequence-group 机制
- Sequence Group 核心规则:
- 配置了 sequence-group 的字段:只有当 sequence 值更大时才更新
- 未配置 sequence-group 的字段:采用 last-value 策略,对于相同主键的记录每次都覆盖
- NULL Sequence 重要边界情况(已通过实际测试验证):
Valid → NULL:不更新,保持旧值 ✅NULL → NULL:字段变为 NULL(非官方文档说明的边界情况)⚠️NULL → Valid:更新为新值 ✅- 最佳实践:始终提供有效的 sequence 值,避免 NULL → NULL 场景
- 多流同步场景:通过为不同字段配置独立的 sequence-group,实现多个数据流独立更新同一张表
- 配置示例:
'fields.update_time.sequence-group' = 'name,address'表示 name 和 address 字段由 update_time 序列控制 - 文档包含完整的测试用例和最佳实践建议
目录
概述
Paimon 的 partial-update 合并引擎允许对表中的特定列进行增量更新,而不影响其他列的数据。通过 sequence-group 机制,可以为不同的字段组指定独立的序列控制字段,实现精细化的更新控制。
基本概念
1. Merge Engine
Paimon 表的合并引擎决定了相同主键的多条记录如何合并。partial-update 是其中一种合并策略。
2. Sequence Group
- 定义:将一个或多个数据字段与一个序列字段(通常是时间戳)关联
- 作用:只有当序列字段的值更大时,相关联的数据字段才会被更新
- 配置语法:
'fields.<sequence_field>.sequence-group' = 'field1,field2,...'
3. 字段分类
在 partial-update 模式下,字段分为两类:
| 字段类型 | 说明 | 更新规则 |
|---|---|---|
| 有序列组的字段 | 通过 sequence-group 配置关联了序列字段 |
只有当序列字段值更大时才更新 |
| 无序列组的字段 | 未配置 sequence-group |
每次都会被新值覆盖(last-value 策略) |
更新规则
规则 1:有序列组的字段更新
配置示例:
CREATE TABLE paimon.test.partial_update_test
(
id INT,
name STRING,
age INT,
name_last_update_time BIGINT
)
TBLPROPERTIES (
'primary-key' = 'id',
'merge-engine' = 'partial-update',
'fields.name_last_update_time.sequence-group' = 'name'
);
行为说明:
name字段由name_last_update_time序列字段控制- 只有当新记录的
name_last_update_time> 旧记录的name_last_update_time时,name才会被更新
测试用例:
-- 第一次插入
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang1', 2, 202509102016);
-- 结果: id=1, name='zhang1', age=2, name_last_update_time=202509102016
-- 第二次插入(相同时间戳)
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang1', 3, 202509102016);
-- 结果: id=1, name='zhang1', age=3, name_last_update_time=202509102016
-- ✅ name 保持不变(时间戳相同)
-- ✅ age 被更新为 3(无序列控制)
-- 第三次插入(更大时间戳)
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang1', 4, 202509102017);
-- 结果: id=1, name='zhang1', age=4, name_last_update_time=202509102017
-- ✅ name 可能被更新(时间戳更大)
-- ✅ age 被更新为 4
-- 第四次插入(更小时间戳)
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang2', 0, 202509102015);
-- 结果: id=1, name='zhang1', age=0, name_last_update_time=202509102017
-- ✅ name 保持不变(时间戳更小,不更新)
-- ✅ age 被更新为 0(无序列控制)
规则 2:无序列组的字段更新
说明:
- 未配置
sequence-group的字段(如上例中的age) - 采用 last-value 策略
- 对于相同主键的记录,每次都会被最新的值覆盖
- 不考虑 sequence 字段的大小,直接使用新值
NULL Sequence 特殊行为
⚠️ 重要发现:NULL Sequence 的边界情况
通过实际测试发现,当 sequence 字段为 NULL 时,Paimon 的行为存在特殊情况:
场景 1:首次插入(sequence 为 NULL)
CREATE TABLE paimon.test.partial_update_test
(
id INT,
name STRING,
age INT,
name_last_update_time BIGINT,
age_last_update_time BIGINT
)
TBLPROPERTIES (
'primary-key' = 'id',
'merge-engine' = 'partial-update',
'fields.name_last_update_time.sequence-group' = 'name',
'fields.age_last_update_time.sequence-group' = 'age'
);
-- 首次插入,age_last_update_time 为 NULL
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang1', 100, 202509102016, CAST(NULL AS BIGINT));
-- ✅ 结果: id=1, name='zhang1', age=100, age_last_update_time=NULL
-- ✅ age=100 成功写入,即使 sequence 为 NULL
场景 2:NULL → NULL(⚠️ 特殊情况)
-- 第二次插入,age_last_update_time 仍为 NULL
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang2', 200, 202509102017, CAST(NULL AS BIGINT));
-- ❗ 结果: id=1, name='zhang2', age=NULL, age_last_update_time=NULL
-- ❗ age 变成了 NULL!(不是保持 100,也不是更新为 200)
关键发现:
- 当旧 sequence 为 NULL,新 sequence 也为 NULL 时
- 该 sequence-group 的字段会被清空为 NULL
- 这与官方文档描述不完全一致
场景 3:NULL → 有效值
-- 第三次插入,给 age 一个有效的 sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang3', 300, 202509102018, 202509102018);
-- ✅ 结果: id=1, name='zhang3', age=300, age_last_update_time=202509102018
-- ✅ age 成功更新为 300
场景 4:有效值 → 更小的有效值
-- 第四次插入,使用更小的 sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang4', 400, 202509102019, 202509102017);
-- ✅ 结果: id=1, name='zhang4', age=300, age_last_update_time=202509102018
-- ✅ age 保持 300(202509102017 < 202509102018,不更新)
场景 5:有效值 → 更大的有效值
-- 第五次插入,使用更大的 sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang5', 500, 202509102020, 202509102020);
-- ✅ 结果: id=1, name='zhang5', age=500, age_last_update_time=202509102020
-- ✅ age 成功更新为 500
场景 6:有效值 → NULL
-- 第六次插入,再次使用 NULL sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang6', 600, 202509102021, CAST(NULL AS BIGINT));
-- ✅ 结果: id=1, name='zhang6', age=500, age_last_update_time=202509102020
-- ✅ age 保持 500(有效值不会被 NULL sequence 覆盖)
NULL Sequence 行为总结表
| 旧 Sequence | 新 Sequence | 字段行为 | 官方文档 | 实测验证 |
|---|---|---|---|---|
| 有效值 | NULL | 不更新,保持原值 | ✅ 有文档 | ✅ 验证通过 |
| NULL | NULL | 清空为 NULL | ❌ 未提及 | ✅ 测试发现 |
| NULL | 有效值 | 更新为新值 | ✅ 符合逻辑 | ✅ 验证通过 |
| 有效值 | 更大的有效值 | 更新为新值 | ✅ 有文档 | ✅ 验证通过 |
| 有效值 | 更小的有效值 | 保持原值 | ✅ 有文档 | ✅ 验证通过 |
测试用例
完整测试脚本
-- 清空表
DROP TABLE IF EXISTS paimon.test.partial_update_test;
-- 创建测试表
CREATE TABLE paimon.test.partial_update_test
(
id INT,
name STRING,
age INT,
name_last_update_time BIGINT,
age_last_update_time BIGINT
)
TBLPROPERTIES (
'primary-key' = 'id',
'merge-engine' = 'partial-update',
'changelog-producer' = 'lookup',
'fields.name_last_update_time.sequence-group' = 'name',
'fields.age_last_update_time.sequence-group' = 'age'
);
-- 测试1:首次插入,age_last_update_time 为 NULL
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang1', 100, 202509102016, CAST(NULL AS BIGINT));
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang1', age=100, age_last_update_time=NULL
-- 测试2:第二次插入,age_last_update_time 仍为 NULL
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang2', 200, 202509102017, CAST(NULL AS BIGINT));
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang2', age=NULL, age_last_update_time=NULL
-- 测试3:给 age 一个有效的 sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang3', 300, 202509102018, 202509102018);
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang3', age=300, age_last_update_time=202509102018
-- 测试4:用更小的 sequence 尝试更新
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang4', 400, 202509102019, 202509102017);
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang4', age=300, age_last_update_time=202509102018 (age 不变)
-- 测试5:用更大的 sequence 更新
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang5', 500, 202509102020, 202509102020);
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang5', age=500, age_last_update_time=202509102020
-- 测试6:再次使用 NULL sequence
INSERT INTO paimon.test.partial_update_test VALUES (1, 'zhang6', 600, 202509102021, CAST(NULL AS BIGINT));
SELECT * FROM paimon.test.partial_update_test WHERE id=1;
-- 期望: id=1, name='zhang6', age=500, age_last_update_time=202509102020 (age 不变)
最佳实践
1. 避免 NULL Sequence
推荐做法:
-- ❌ 不推荐:使用 NULL
INSERT INTO table VALUES (1, 'name', 100, CAST(NULL AS BIGINT));
-- ✅ 推荐:始终提供有效的时间戳
INSERT INTO table VALUES (1, 'name', 100, UNIX_TIMESTAMP() * 1000);
2. 使用时间戳类型
推荐配置:
CREATE TABLE example_table (
id INT,
data STRING,
update_time TIMESTAMP(3), -- 使用 TIMESTAMP 类型
PRIMARY KEY (id) NOT ENFORCED
) TBLPROPERTIES (
'merge-engine' = 'partial-update',
'fields.update_time.sequence-group' = 'data'
);
3. 字段分组策略
根据业务需求合理分组:
CREATE TABLE user_profile (
user_id BIGINT,
-- 基础信息组
name STRING,
age INT,
basic_info_update_time BIGINT,
-- 扩展信息组
address STRING,
phone STRING,
extended_info_update_time BIGINT,
PRIMARY KEY (user_id) NOT ENFORCED
) TBLPROPERTIES (
'merge-engine' = 'partial-update',
'fields.basic_info_update_time.sequence-group' = 'name,age',
'fields.extended_info_update_time.sequence-group' = 'address,phone'
);
4. 多流更新场景
在多数据源同步场景下,使用不同的 sequence-group 避免冲突:
-- 场景:主从表同步
CREATE TABLE target_table (
id INT,
-- 主表字段
main_name STRING,
main_value INT,
main_seq TIMESTAMP(3),
-- 从表字段
slave_info STRING,
slave_score INT,
slave_seq TIMESTAMP(3),
PRIMARY KEY (id) NOT ENFORCED
) TBLPROPERTIES (
'merge-engine' = 'partial-update',
'fields.main_seq.sequence-group' = 'main_name,main_value',
'fields.slave_seq.sequence-group' = 'slave_info,slave_score'
);
社区讨论
相关 Issue 和 PR
1. PR #6345 - 性能优化(2025年9月)
- 链接:https://github.com/apache/paimon/pull/6345
- 标题:Skip processed sequence group fields to improve performance
- 关键内容:
- 明确提到 "skip null sequence group fields"
- 优化了 NULL sequence 字段的处理性能
- 当 sequence 字段为 NULL 时,跳过该 sequence-group 的处理
- 状态:Open
2. Issue #5852 - NULL 字段处理 Bug(2025年7月)
- 链接:https://github.com/apache/paimon/issues/5852
- 标题:[Bug] Paimon config-item :partial-update.ignore-null-field not valid
- 关键内容:
partial-update.ignore-null-field配置不生效- 主从表同步场景中 NULL 值处理问题
- 状态:Open
3. Issue #5179 - 聚合函数一致性(2025年2月)
- 链接:https://github.com/apache/paimon/issues/5179
- 标题:[Bug] aggregate function in partial update inconsistency
- 关键内容:
- sequence-group 与聚合函数结合时的行为
- 当 sequence 值较小时应该忽略更新
- 状态:Open
未解决的问题
根据本文档的测试发现,以下问题在官方文档中未明确说明:
-
NULL → NULL 的边界情况
- 官方文档说明:sequence 为 NULL 时字段不更新
- 实际测试:当旧值也是 NULL 时,字段会被清空
- 建议:在社区提交 issue 讨论此行为是否符合预期
-
NULL sequence 的语义
- 是表示"不提供更新信息"?
- 还是表示"清空该字段"?
- 需要社区明确定义
参考资料
官方文档
- Partial-Update 文档:https://paimon.apache.org/docs/master/primary-key-table/merge-engine/partial-update/
- Merge Engine 概述:https://paimon.apache.org/docs/master/primary-key-table/merge-engine/
关键引用
"null values are not overwritten in the process"
— Paimon 官方文档
"when the sequence field itself is NULL, the associated columns are not updated"
— Paimon Partial-Update 文档
GitHub 资源
- Apache Paimon GitHub:https://github.com/apache/paimon
- Issues 搜索:https://github.com/apache/paimon/issues
版本信息
- 测试环境:Spark 3.3.4 + Paimon 0.8
- 文档编写日期:2025年12月
- 作者:基于实际测试和官方文档整理