Apache Paimon Partial-Update

Apache Paimon Partial-Update

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 序列控制
  • 文档包含完整的测试用例和最佳实践建议

目录

  1. 概述
  2. 基本概念
  3. 更新规则
  4. NULL Sequence 特殊行为
  5. 测试用例
  6. 最佳实践
  7. 社区讨论
  8. 参考资料

概述

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

未解决的问题

根据本文档的测试发现,以下问题在官方文档中未明确说明:

  1. NULL → NULL 的边界情况

    • 官方文档说明:sequence 为 NULL 时字段不更新
    • 实际测试:当旧值也是 NULL 时,字段会被清空
    • 建议:在社区提交 issue 讨论此行为是否符合预期
  2. 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月
  • 作者:基于实际测试和官方文档整理

Paimon Compaction 详解 2025-12-24
域名转发架构文档 2025-12-06

评论区