1. 一句话理解 Data Evolution

Paimon Data Evolution 是一种面向 无主键 Append Table 的列级更新机制:
  • 每一行由 Paimon 分配稳定的 _ROW_ID;
  • 更新时只写发生变化的列,不重写未变化的列或大对象;
  • 同一逻辑行的不同列可以来自不同物理文件;
  • 读取时按 _ROW_ID 对齐,并按文件版本为每一列选择最新值;
  • 多次更新产生的列文件最终通过 compaction 合并。
它特别适合如下流水线:
  1. 先写入原始数据;
  1. 后续异步补充标签、描述、特征、Embedding 等列;
  1. 原始图片、音视频等大对象保持不动;
  1. 持续构建或刷新向量、全文、标量索引。

2. Data Evolution 不等于 Schema Evolution

这两个概念很容易混淆。
概念
解决的问题
示例
Schema Evolution
表中有哪些列、列叫什么、列类型是什么
ALTER TABLE ADD COLUMN caption STRING
Data Evolution
已有行的新列值或旧列值如何补充、替换
为历史图片计算 caption 并写回
执行:
只会修改 Schema,不会自动为历史行计算 caption。还需要通过 UPDATE、MERGE INTO、PyPaimon shard update 等方式执行数据回填。

3. 与普通 Append Table、主键表的区别

特性
普通 Append Table
Data Evolution Table
Primary Key Table
Primary Key
无
无
有
相同业务键再次 INSERT
新增一行
新增一行
按合并引擎更新
行身份
通常不暴露稳定行身份
_ROW_ID
Primary Key
更新方式
通常需要重写数据
按 _ROW_ID 做列级更新
按 Primary Key 合并
未更新列
随文件一起重写
保留旧列文件
由 merge engine 决定
BLOB/Vector 专用存储
不适用
核心场景
部分场景支持
典型用途
日志、事件
多模态和异步特征生产
CDC、业务实体状态
最重要的区别是:
Data Evolution 表中的业务字段 id 不是 Primary Key。
例如:
这会产生两条不同的逻辑行,并获得两个不同的 _ROW_ID。

4. 建表前提与硬约束

4.1 必须同时开启的选项

这两个选项都是不可变选项,应在建表时确定。

4.2 表模型约束

Data Evolution 表必须满足:
  1. 是 Append Table;
  1. 没有 Primary Key;
  1. bucket = -1,即 bucket-unaware;
  1. 必须启用 Row Tracking;
  1. 不能启用 clustering.incremental。

4.3 Spark 建表示例

deletion-vectors.enabled 不是普通列更新的必要条件,但执行行级 DELETE 或 MERGE INTO ... DELETE 时需要开启。

4.4 Flink 建表示例


5. 核心概念一:Row ID

5.1 _ROW_ID 是什么

Row Tracking 为每一条 Append Table 记录分配一个 BIGINT 类型的内部行 ID。
可以通过 $row_tracking 系统表查看:

5.2 Row ID 如何分配

普通追加文件在写出时可以还没有 firstRowId。提交快照时,提交层根据上一个快照的 nextRowId 为新文件分配连续区间。
假设两个新文件分别有 3 行和 2 行:
文件的 Row ID 范围定义为:
文件中第 i 个位置对应:

5.3 Row ID 不是永久业务 ID

以下操作会保留 Row ID:
  • 普通列更新;
  • 列回填;
  • 逻辑删除;
  • 普通 Data Evolution compaction;
  • compaction 中拆分大文件。
以下操作可能改变或移除 Row ID:
  • materialize_deletion_vectors;
  • reassign_row_id;
  • overwrite;
  • 分区删除或分区替换。
因此:
_ROW_ID 是存储层行定位符,不应直接当成永久业务主键。
如果其他表持久化保存了 _ROW_ID,必须与 Row ID 重分配、Deletion Vector 物化等维护任务协调。

6. 核心概念二:文件组和范围契约

Data Evolution 的正确性依赖严格的 Row ID Range 文件布局。

6.1 Normal File

Normal File 使用普通表文件格式,例如 Parquet 或 ORC,存储:
  • 全部普通列;或者
  • 一次更新选择的部分普通列。
任意两个存活的 Normal File,其 Row ID Range 必须:
  • 完全相同;或者
  • 完全不相交。
不允许部分重叠,也不允许一个严格包含另一个。
Row ID Range 完全相同的一组 Normal File 构成一个逻辑文件组。

6.2 Dedicated File

Dedicated File 是专用格式文件,例如:
  • BLOB 文件;
  • Vector Store 文件。
Dedicated File 的范围可以小于 Normal File,但必须完全包含在某一个 Normal Range 内:
例如 Normal Range 为 [0, 5] 和 [6, 9]:
Dedicated Range
是否合法
原因
[0, 5]
是
与一个 Normal Range 完全相同
[2, 4]
是
完全包含在 [0, 5]
[4, 7]
否
跨越两个 Normal Range
[0, 9]
否
不能依赖两个 Normal Range 的并集

6.3 为什么普通列更新必须重写完整范围

假设原始文件:
执行:
不能只写一行 [rowId=1, b=22],而是要生成:
逻辑结果由两个文件合并:
结果:
这说明 Data Evolution 节省的是 未更新列的重写成本,而不是把一次普通列更新缩小到单行文件。
更新一行,也可能需要处理其所在文件范围内的所有行。

7. 文件元数据

理解以下 DataFileMeta 字段非常重要:
字段
含义
firstRowId
文件第一个逻辑位置的 Row ID
rowCount
物理位置数,包含被 Deletion Vector 隐藏的位置
schemaId
写出文件时使用的 Schema
writeCols
文件实际写入的列或嵌套字段路径
minSequenceNumber
文件中最小数据版本
maxSequenceNumber
文件中最大数据版本
writeColsSequences
compaction 后每个物理字段保留的最大版本
extraFiles
Sidecar 等与主文件对齐的附属文件

7.1 writeCols

例如:
必须结合文件自己的 schemaId 解析 writeCols,不能只按最新表 Schema 的字段名解析。
这是为了正确处理:
  • 字段重命名;
  • 新增字段;
  • 不同 Schema 版本下的字段 ID;
  • 嵌套字段演进。

7.2 writeCols = null 的优化编码

开启:
当 Normal File 包含全部非 Dedicated 列时,可以省略 writeCols。
该选项默认关闭,主要原因是老版本 Reader 可能把 null 错误理解为“文件包含表中全部列”。启用前应升级所有 Reader、Writer、Compactor 和维护任务。

8. 读取流程

8.1 总体流程

8.2 主要处理层次

8.3 Scan 阶段

Data Evolution Scan 不能像普通 Append Scan 一样独立判断每个文件。
原因是:
  • 老文件的某个列可能已被新文件覆盖;
  • 老文件统计信息不能用来判断新版本列值;
  • 查询投影可能只需要一个文件组中的部分列文件;
  • 即使查询列在所有文件中都不存在,也要保留一个文件来提供正确行数并补 NULL;
  • 开启 Deletion Vector 时必须保留 Anchor File。
因此 Scan 会:
  1. 按重叠 Row ID Range 对文件分组;
  1. 合并最新列 Provider 的统计信息;
  1. 基于逻辑最新值做文件组级过滤;
  1. 根据 Projection 和 Predicate 需要,删除不贡献列的物理文件;
  1. 必要时保留 Anchor 或行数代表文件。

8.4 Split 生成

Split 规划会先把重叠 Row ID Range 合并成不可拆的逻辑组,再根据目标 Split 大小做打包。
这样可以保证需要一起进行列合并的文件不会被分配到不同 Split。

8.5 Provider 选择

同一文件组中的 Normal File 按新版本优先排序。对每个请求字段,Reader 选择第一个能够提供该字段的文件。
例如:
读取 id,b,c 时:
这不是按业务键进行 Join,而是按 Row ID 位置做列拼装。

8.6 缺列、NULL 和 BLOB Placeholder

三种情况语义完全不同:
表示
读取语义
文件没有写该列
文件不提供新值,继续找旧 Provider
文件写了该列且值为 NULL
最新值就是 NULL,不能回退
BLOB Placeholder
保留旧 BLOB,继续向旧版本查找
不要使用 NULL 表示“保持旧 BLOB 不变”。BLOB Placeholder 是内部写入语义,由 Paimon Writer 管理。

8.7 Filter Pushdown 为什么更谨慎

多文件位置合并要求所有列 Reader 保持严格对齐。
如果只对其中一个 Reader 下推行过滤,可能出现:
此时列会错位。
因此:
  • 单文件读取可以正常执行文件索引和格式级下推;
  • 可证明安全的合并组可共享一个 Bitmap Selection;
  • 其他合并读取通常只能用索引跳过整个文件组;
  • 系统字段的物理值可能因 Row ID 重分配而过期,不直接下推到物理文件。

8.8 Row Sidecar

可选配置:
Writer 会为 Normal File 生成行存 Sidecar,用于加速稀疏 Row ID 读取。
默认使用 Sidecar 的两个门槛:
只有同时满足以下条件时才使用 Sidecar:
  1. 选中行数不超过 4096;
  1. 选中行比例不超过 5%;
  1. 不是全文件读取;
  1. 文件存在唯一的 Row Sidecar;
  1. 不是 BLOB 或 Vector Store 文件。
Row Sidecar 适合全局索引返回少量 Row ID 后的点查,不适合大范围扫描。

9. 普通列更新流程

9.1 端到端流程

9.2 为什么关闭 File Rolling

部分列文件必须与原 Normal Range 完全对齐。因此 Spark 和 Flink 的 Data Evolution Writer 都会禁用普通文件滚动:
Flink 还会关闭 Append Write Buffer,以避免中途 Flush 导致文件提前关闭。

9.3 未命中行也要写

一个范围内只更新少量行时,Writer 会:
  • 匹配行写新值;
  • 未匹配行复制旧值;
  • 被 Deletion Vector 隐藏的位置仍然保留占位;
  • 最终写出的行数必须等于原范围 rowCount。
这保证其他列文件仍可按位置对齐。

9.4 更新分区列

Matched partial update 不支持修改分区列。
分区决定文件所在目录和文件组归属,直接修改分区列不是简单的列 Provider 替换,而是删除旧行并向另一个分区插入新行。

10. Spark UPDATE 与 MERGE INTO

10.1 UPDATE

Spark 的 Data Evolution UPDATE 内部会构造成基于 _ROW_ID 的 self-merge。
当条件可以安全放入 MergeRows 时,可优化为:
避免普通 self join、shuffle 和 sort。

10.2 MERGE INTO

语义:
  • Matched Update:保留现有 _ROW_ID,写部分列文件;
  • Not Matched Insert:追加新行,提交时分配新 _ROW_ID;
  • Matched Delete:需要 Deletion Vector;
  • Not Matched By Source Delete:需要 Deletion Vector。
Spark Data Evolution MERGE 支持的 Action 形态:
Clause
支持的 Action
WHEN MATCHED
UPDATE、DELETE
WHEN NOT MATCHED
INSERT
WHEN NOT MATCHED BY SOURCE
DELETE
WHEN NOT MATCHED BY SOURCE 需要 Spark 3.4 或更高版本。

10.3 _ROW_ID 快捷路径

如果匹配条件为:
Paimon 可以跳过普通业务条件 Join,直接定位目标范围。
Self-merge _ROW_ID 快捷路径只支持 WHEN MATCHED,不能同时包含:
  • WHEN NOT MATCHED;
  • WHEN NOT MATCHED BY SOURCE。

10.4 File Pruning

默认开启。Paimon 会先收集实际被更新的目标文件 ID,从后续部分列写入中裁剪未触碰文件。
如果预期目标分区的大多数文件都会被更新,收集文件 ID 的成本可能高于收益,可以关闭。

10.5 Spark UPDATE 冲突重试

Spark V1 UPDATE 默认配置:
只有确定性条件和确定性更新表达式才适合自动重试。

11. Flink Data Evolution 更新

Flink 提供专门的 procedure:
该 procedure 是精简版 Merge:
  • 支持匹配后更新;
  • 不追加 source 中未匹配的行;
  • 不执行删除;
  • 目标表不能是空表;
  • 更新前固定一个 base snapshot;
  • 输入按 firstRowId Shuffle,并按 _ROW_ID 排序;
  • 每个范围补齐未更新行后写部分列文件。
删除应使用:
  • Flink 1.17+ Batch SQL DELETE FROM;或者
  • delete Action。

12. PyPaimon 更新方式

PyPaimon 提供多种 API。

12.1 按 Predicate 更新

12.2 按 Row ID 更新

输入只需要 _ROW_ID 和待更新列:

12.3 Shard Update

适合批量计算派生列:
必须遵守:
  1. 输出批次与输入批次行数相同;
  1. 顺序相同;
  1. 不能过滤、排序、复制行;
  1. 不同 Worker 使用不同 shard index,但 shard count 必须一致。

13. Nested Field Evolution

默认情况下,更新单位是顶层列。
例如:
更新 payload.caption 时,默认会把整个 payload 当作更新列。
启用:
后,writeCols 可以记录:
Reader 会按叶子字段选择最新 Provider,并重新组合 Struct。

13.1 当前限制

  • 支持把一层 Struct 的直接子字段分散到不同文件;
  • 不支持把更深层的同一个子 Struct 再分散到多个文件;
  • 顶层 Struct 整体写入与任意子字段写入会产生冲突;
  • 是否支持具体赋值语法还取决于 Spark/Flink Engine。

13.2 升降级风险

该选项具有明显的混合版本风险:
  1. 必须先升级所有 Reader、Writer、Compactor 和维护任务;
  1. 只能通过持久化表选项开启;
  1. 开启后不能关闭或删除;
  1. 一旦提交了嵌套路径文件,旧版本 Reader 无法正确重建;
  1. 二进制降级不安全。

14. BLOB 和 Vector 文件

14.1 BLOB

对于托管 BLOB:
  • Normal File 提供普通列;
  • BLOB 数据写入专用 BLOB 文件;
  • BLOB 文件可按独立大小滚动;
  • 每个 BLOB 文件必须包含在一个 Normal Range 中;
  • 新 BLOB 版本可以只覆盖部分范围;
  • Placeholder 表示沿用旧值;
  • 显式 NULL 表示把值更新为 NULL。

14.2 Vector

专用 Vector File 可以独立于 Normal File 滚动,但读取一个完整向量列时,选中的 Vector 文件应按 Row ID 顺序完整覆盖对应 Normal Range。
当前维护限制:
  • Dedicated Vector File Compaction 尚未支持;
  • 包含 Dedicated Vector File 的组不能执行 Deletion Vector Materialization。

14.3 Flink BLOB 更新注意

Flink procedure 支持多种 BLOB 更新形态,但托管 BLOB 值会进入 Merge Sort Buffer。
单行托管 BLOB 如果大于 write-buffer-size(默认 256 MB),任务会失败。

15. 删除与 Deletion Vector

15.1 为什么不能直接移除位置

假设范围为 [0, 5],删除 Row ID 2。
如果直接把物理位置移除,后面的 Row ID 会错位:
其他列文件仍然有 6 个位置,无法继续按位置合并。
因此普通删除只写 Deletion Vector:

15.2 Anchor File

同一 Row Range Group 中最老的 Normal File 是 Anchor File。
排序规则:
Deletion Vector 关联到 Anchor,读取时应用到整个逻辑文件组,从而同时隐藏:
  • 普通列;
  • BLOB;
  • Vector。

15.3 Spark 删除

或者:
必须配置:

15.4 Flink 删除

Flink 1.17+ 可以在 Batch 模式执行:
也可以提交 delete Action。Action 会固定一个 $row_tracking Snapshot,找到匹配 Row ID 并写入 Deletion Vector。

16. 并发与冲突检测

Data Evolution 的并发冲突分为两类。

16.1 Layout Conflict

操作基于旧 Snapshot 计划时,认为 Normal Range 是:
在提交前,另一个 compaction 把它们合并为:
Row ID 没变,但文件边界变了。旧 Writer 生成的 [0, 5] 部分列文件不再符合当前布局。
这属于 Row ID Range/Layout Conflict。

16.2 Logical Update Conflict

冲突判断不仅看 Range,还看写入字段 ID:
两个写入范围相同,但列不重叠,可以不构成列更新冲突。
即使两个 Predicate 命中了不同业务行,也会冲突,因为两个文件都覆盖完整 [0,5] 范围,并且都替换 b。
冲突条件可简化为:
Nested Field 开启时,冲突会细化到叶子字段;但整列 Struct 更新会与其任意子字段更新冲突。

16.3 Compaction 后 Rebase

Spark MERGE 可以在并发 compaction 改变边界后,把已写出的普通部分列文件重新映射到最新范围,而不重新执行 source 和 join。
PyPaimon 也支持类似恢复,大小由:
限制。设置为 0 B 可关闭。
以下情况不执行这种恢复:
  • 启用了 Deletion Vector;
  • 同一提交包含已有行的 BLOB 文件;
  • 同一提交包含已有行的 Vector 文件;
  • 存在真正的同列并发更新冲突;
  • 当前文件总大小超过 PyPaimon 配置上限。

17. Compaction

17.1 普通 Data Evolution Compaction

目的:
  • 合并多次部分列更新;
  • 降低读取文件数;
  • 合并相邻 Normal Range;
  • 保留 Row ID;
  • 保留逻辑删除;
  • 保留字段版本信息。
例如:
逻辑值不变,Row ID 不变。
普通 compaction 不会物理删除被 Deletion Vector 隐藏的位置,因为这会破坏未重写列文件的对齐。

17.2 合并相邻范围

普通 compaction 可以:
Row ID 不变,但边界改变,这也是并发部分列写入可能遇到 Layout Conflict 的原因。

17.3 拆分大 Normal File

配置:
只有文件严格大于:
才会因为“大文件”被选择。
拆分时:
  • 只重写 Normal File;
  • Row ID 保持不变;
  • BLOB/Vector 文件不重写;
  • 切分点不能落在 Dedicated File Range 内;
  • 实际输出大小可能大于目标值;
  • Dedicated Range 如果阻止所有安全切分,则不会仅因大小触发任务。

17.4 BLOB Compaction

默认关闭,应根据 BLOB 文件碎片情况单独决定是否开启。
Vector Dedicated Compaction 当前未支持。

18. Deletion Vector Materialization

当需要真正移除删除位置时,运行:
该过程会:
  1. 读取受影响文件组;
  1. 应用 Deletion Vector;
  1. 只写存活行;
  1. 为存活行分配新 Row ID;
  1. 删除已物化的 Deletion Vector;
  1. 删除受影响的 Global Index;
  1. 对受影响 Row Range 做并发冲突检查。
因此它与普通 compaction 的关键区别是:
操作
删除位置
Row ID
普通 Compaction
保留逻辑删除位置
保留
Materialize DV
物理移除删除位置
重新分配
当前不支持物化包含 Dedicated Vector File 的文件组。
历史 Snapshot 或 Tag 仍可能引用旧文件。只有引用过期并执行 Snapshot Expiration 后,存储空间才会被真正回收。

19. Global Index 与 Data Evolution

19.1 为什么索引会过期

Global Index 通常保存:
Data Evolution 更新列时 Row ID 不变,但列值可能改变:
旧索引仍把 a200 指向 Row ID 100,因此索引已过期。

19.2 更新索引列的策略

值
行为
THROW_ERROR
默认值,拒绝更新已建立 Global Index 的列
DROP_PARTITION_INDEX
提交更新时删除受影响分区索引
IGNORE
保留旧索引,之后必须刷新或重建
使用 IGNORE 时:
  • Spark、Flink 增量构建可根据索引源元数据定位发生变化的 Row Range;
  • 老索引没有 Data Evolution Source Metadata 时不能自动增量刷新;
  • PyPaimon 当前的增量构建主要补未覆盖范围,已覆盖行发生值更新后应显式删除并重建相关索引。

19.3 Search Mode 与覆盖范围

索引覆盖和索引新鲜度是两个不同概念:
  • Coverage:这个 Row ID Range 是否有索引文件;
  • Freshness:索引中的值是否仍然是最新列值。
detail 等搜索模式可以补扫未覆盖范围,但不能自动识别“覆盖范围内的旧值”。

20. 与 Lance、LanceDB 和 Apache Iceberg 的对比

Data Evolution 并不是 Paimon 独有的需求。随着湖仓系统开始承载图片、音视频、Embedding、模型分数和大量派生特征,多个项目都在解决同一个问题:
如何只写发生变化的列,同时保持行身份、快照隔离、索引正确性和可维护性?
Lance 已经具备成熟的整列追加和整列替换能力,并正在设计更细粒度的 Data Overlay;LanceDB 在其上提供面向特征工程和检索的高级接口。Iceberg 现有 Copy-on-Write 和 Merge-on-Read 仍主要工作在行粒度,但 V4 规范和社区开发计划已经明确加入 Column File。
本章以 2026-10-17 时各项目的官方文档、格式规范、主分支行为和公开 Roadmap 为依据。由于 LanceDB 与 Lance Core 的发布节奏并不完全同步,对只存在于最新格式设计中的能力会单独标记为实验性,不假设上层产品已经公开支持。
这里需要区分三个 Lance 相关概念:
名称
含义
Lance File Format
支持独立列文件、Fragment、随机访问和索引的底层格式与 Dataset 实现
LanceDB
基于 Lance 的检索数据库和 SDK,提供 Update、Merge Insert、Feature Engineering、Vector/FTS 等高级能力
Paimon file.format=lance
Paimon 使用 Lance 编码读写物理数据文件,表元数据、快照、Row ID、冲突和 Compaction 仍由 Paimon 管理
因此:
在 Paimon 中选择 Lance 作为文件格式,不等于把 Paimon 表转换成 Lance Dataset,也不会自动使用 Lance 的 Fragment、Transaction 或 Stable Row ID 语义。

20.1 三者共同要解决的问题

三种设计都面对同样的写放大问题。
假设一行包括:
如果只更新 model_score,传统 Row Rewrite 可能重写整行,甚至重新处理大型图片。列级更新希望把写入缩小为:
三者的共同思想是:
  1. 逻辑行不再要求全部列位于一个物理文件;
  1. 数据文件需要某种稳定的位置或行身份;
  1. Reader 负责把多个物理来源拼成逻辑行;
  1. 更新文件积累后需要 Compaction;
  1. 更新过的索引不能继续无条件使用;
  1. Snapshot 必须原子切换,不能暴露半完成状态。

20.2 Lance 的 Fragment 模型

Lance 的核心组织单位是 Fragment:
Lance 把 Fragment 定义为“表示同一批行的多个列文件集合”。
一个 Fragment 的多个 Data File:
  • 表示同一批物理行;
  • 每个文件通过 fields 声明自己保存的 Field ID;
  • 缺失列在没有其他 Provider 时读作 NULL;
  • 文件按物理位置对齐;
  • Fragment 通过 physical_rows 记录原始物理位置数;
  • Deletion File 记录被逻辑删除的本地 Row Offset。
这与 Paimon 的 Normal Row ID Range Group 很相似:
但两者的身份边界不同:
Paimon
Lance
文件通过 firstRowId + rowCount 建立对齐
文件直接归属于同一个 Fragment
相同范围的 Normal File 构成文件组
Fragment 元数据显式包含多个列文件
Dedicated File 可覆盖 Normal Range 的子区间
Fragment Base Data File 通常描述同一组物理行
Compaction 可以合并相邻 Row ID Range
Compaction 可以合并或重写 Fragment

20.3 Lance 增加和回填新列

Lance 的新增列能力支持三种重要模式。
20.3.1 只增加 Schema
该模式只更新元数据。旧 Fragment 不包含新列文件,Reader 为历史行补 NULL。它与 Paimon 单纯增加一个可空列的初始效果相同:只增加列定义,不自动计算历史值。
20.3.2 增加列并立即回填
Lance 可以通过 SQL 表达式计算新列,也可以通过批处理函数计算 Embedding、模型输出等复杂结果。耗时函数还可以使用 Checkpoint,失败后从已完成批次继续。
实现会逐个 Fragment 读取必要输入列,并为新列写出额外 Data File,而不重写旧列文件。
其逻辑结构为:
与 Paimon Shard Update 的共同点是:
  • 按已有物理顺序读取;
  • 输出行数和顺序必须保持一致;
  • 只写新列;
  • 不复制大型未更新列。
Lance 的 API 对 Batch UDF 和 Checkpoint 支持更直接;Paimon 的优势是同一存储语义已经接入 Spark SQL、Flink Procedure 和 PyPaimon。

20.4 Lance 整列替换

Lance 的整列替换事务用于替换现有 Fragment 的列文件。它把“目标 Fragment”和“新的列文件”作为一个原子替换单元提交。
要求:
  1. 目标字段必须已经存在于 Dataset Schema;
  1. 当前 API 按顶层列整体替换,不能只暂存一个嵌套叶子字段;
  1. 输入必须产生与 Fragment physical_rows 完全相同的行数;
  1. 输入包含被删除的物理位置,不能先应用 Deletion File 后缩短;
  1. 新文件与 Fragment 按物理位置对齐;
  1. 调用方需要基于正确 Read Version 提交,才能检测同字段并发替换。
这与 Paimon Normal Partial File 的契约高度相似:

20.5 Lance 列重写与 Paimon 部分列 MERGE

Lance Merge Insert 提供两种写入形态:
列重写模式的语义是:
  • Source 只携带 Join Key 和待更新列;
  • 不读写 Source 省略的列;
  • 在命中的 Fragment 上附加新的列文件;
  • 新列文件仍覆盖整个 Fragment;
  • 未命中行需要保留原值;
  • 不移动逻辑行,也不新增 Fragment;
  • Commit 会声明这是保持行位置不变的列重写;
  • 提交元数据记录被修改字段,用于处理索引覆盖失效;
  • 提交元数据记录实际命中位置,用于更新 Stable Row ID 的行级版本信息。
这几乎就是 Paimon Data Evolution MERGE 的 Lance 对应物:
Lance 文档还给出了一个重要成本模型:
可以用近似条件选择:
例如:
只要一个 Fragment 中更新超过约 0.1% 的行,整列替换就可能比移动完整行更便宜。
Paimon Data Evolution 当前主要采用整列范围写法,没有在普通 UPDATE 中自动切换为 Row Rewrite。其优势是模型统一、未更新 BLOB 不动;代价是极稀疏普通列更新仍会处理完整 Normal Range。

20.6 Lance 普通 UPDATE 与 Paimon UPDATE 不完全相同

Lance 普通行更新默认采用 Row Rewrite。
其执行方式是:
  1. 找出匹配行;
  1. 将旧物理行加入 Deletion File;
  1. 写出包含更新后完整行的新 Fragment;
  1. Stable Row ID 开启时把原 Row ID 复制给新物理行;
  1. 新地址替换旧地址。
因此不能笼统地说:
Lance 的所有 UPDATE 都是列级更新。
更准确的说法是:
Lance 操作
物理更新粒度
新增列并回填
给每个 Fragment 增加新列文件
整列替换
替换 Fragment 的完整列文件
Merge Insert 列重写
更新选中列,但文件覆盖完整命中 Fragment
普通行更新
默认删除旧物理行并写完整新行
Data Overlay
只写变化 Cell,目前实验性
Paimon Data Evolution 表的 Spark UPDATE 和匹配型 MERGE 默认走部分列文件语义,这是两者 API 默认行为的重要差异。

20.7 Stable Row ID 对比

Paimon
Paimon Row ID 以文件范围元数据为基础:
普通更新和普通 Compaction 保留 Row ID。Deletion Vector Materialization 会移除位置,并为重写范围中的存活行重新分配 Row ID。
Lance
Lance 同时区分:
未开启 Stable Row ID 时,_rowid 实际等同于物理 Row Address,会在 Compaction 或 Row Rewrite 后变化。
开启 Stable Row ID 后:
  • 新行从 Manifest 的 next_row_id 单调分配;
  • Fragment 保存 Row ID Sequence;
  • Update 把同一 Stable Row ID 搬到新物理地址;
  • 旧物理位置通过 Deletion File 隐藏;
  • Compaction 可以删除物理墓碑,但保留存活行的 Stable Row ID;
  • Row ID Sequence 可以内联在 Manifest,也可以溢写到隐藏列;
  • _row_created_at_version 和 _row_last_updated_at_version 支持增量处理和 Change Feed。
对比:
行身份特性
Paimon
Lance
默认是否开启
Data Evolution 必须开启 Row Tracking
默认可不开启 Stable Row ID
物理地址
不作为主要公开模型
fragmentId + localOffset
逻辑 ID 存储
主要通过 firstRowId + position 推导
Fragment Row ID Sequence
Update 后
保留原 ID
开启 Stable ID 时保留
普通 Compaction
保留 ID
开启 Stable ID 时保留
物理删除墓碑
Materialize DV 会重分配 ID
可重写物理文件并保留存活行 ID
行版本元数据
主要依赖 Snapshot 和 Sequence
有显式 created/last-updated version sequence
这里 Lance 的 Stable Row ID 更接近“永久逻辑行身份”;Paimon _ROW_ID 更接近“在不执行重分配维护时稳定的存储层位置标识”。

20.8 删除与 Compaction 对比

两者都采用逻辑删除文件:
共同点:
  • 删除不立即重写大型数据文件;
  • 删除记录的是物理位置;
  • Reader 在输出前过滤这些位置;
  • 历史版本仍可以引用旧数据;
  • 真正回收空间需要重写与清理历史版本。
主要区别:
项目
Paimon
Lance
删除定位
Row Range Group 的 Anchor File
Fragment 本地 Row Offset
普通 Compaction
保留逻辑删除位置,以维持未重写列对齐
可以重写整个 Fragment 并移除删除位置
删除物化后的 Row ID
当前 Paimon 会重分配受影响范围的 Row ID
Stable Row ID 开启时可保留存活行 ID
专用列文件约束
BLOB/Vector 可能阻止切分或 DV Materialization
重写 Fragment 时统一处理 Fragment 文件与 Row ID Sequence
这种差异来自文件组织:
  • Paimon 普通 Compaction 可以只重写 Normal File,同时保留 Dedicated File;
  • Lance Fragment Rewrite 可以把所有当前逻辑列重新组织到新 Fragment,并通过 Row ID Sequence 保留逻辑身份。

20.9 Lance Data Overlay:比整列更新更细的未来方向

Lance 的最新格式设计中已经包含 Data Overlay 的元数据、读取、事务、冲突和维护规则,但官方格式文档明确标记:
所以它代表正在开发的方向,不能当成当前稳定生产 API。
Data Overlay 的目标是只写变化的 Cell:
例如,当一次更新只改变 Fragment 中第 1、3 个位置的 age 时,Overlay 可以只保存:
而不保存同一 Fragment 中未命中的 age。
读取规则:
这与 Paimon 的区别非常关键:
从粒度看:
Overlay 会降低极稀疏更新的写放大,但会增加:
  • Coverage Bitmap;
  • Overlay 文件打开;
  • 读取时版本查找;
  • Base 与 Overlay 合并;
  • 索引排除和重新计算;
  • Overlay Compaction。
Lance 规划两种 Overlay Compaction:
Paimon 如果未来要支持稀疏普通列更新,也会面临类似设计选择:
  • 是否增加 Cell Coverage;
  • 如何表达显式 NULL 与未覆盖;
  • 如何与 Deletion Vector 叠加;
  • 如何维护字段级索引新鲜度;
  • 如何限制 Overlay 层数;
  • 如何在 Compaction 中折叠版本。

20.10 索引一致性对比

Paimon
Paimon Global Index 使用 Row ID Range,更新索引列时由:
决定:
  • THROW_ERROR;
  • DROP_PARTITION_INDEX;
  • IGNORE 后刷新。
增量 Index Planner 根据字段版本和 Row Range 判断需要重建的索引范围。
Lance
Lance 索引通常与 Fragment 和 Row Address/Stable Row ID 关联。
对于列文件替换:
  • 被修改字段列表用于识别失效的索引覆盖;
  • 覆盖这些字段的索引移除受影响 Fragment Coverage;
  • 查询退回 Flat Scan;
  • 后续 Optimize 或 Reindex 重新纳入。
对于 Row Rewrite:
  • 旧地址通过 Deletion File 隐藏;
  • 新物理行可能暂时未进入已有索引;
  • Flat Tail 保证仍可搜索;
  • Optimize 把新行折入索引。
实验性 Overlay 设计则使用:
计算字段级 Exclusion Set:
  1. 从索引结果排除 Overlay 更新过的位置;
  1. 用当前值重新扫描和判断这些位置;
  1. 将仍然匹配的行补回结果。
相比之下:
设计
优点
代价
Paimon Throw/Drop/Rebuild
语义简单,易保证正确
更新索引列可能阻塞或导致较大重建
Lance Fragment Coverage Pruning
允许保留其他 Fragment 的索引
受影响 Fragment 暂时退化为 Flat Scan
Lance Overlay Exclusion
可继续利用旧索引大部分结果
查询链路更复杂,需要排除和重新计算

20.11 Paimon 与 Lance 的能力矩阵

维度
Paimon Data Evolution
Lance/LanceDB
核心定位
湖仓 Append Table 的列级演进
ML/检索优化的列式 Dataset 和数据库
物理分组
Row ID Range Group
Fragment
新增空列
Schema Evolution
Metadata-only Schema Change
新列回填
Spark/Flink/Python 部分列写
SQL、Batch UDF、Stream/Reader
已有列整列替换
部分列文件
Fragment Column Replacement
匹配型列更新
默认 Data Evolution 路径
Merge Insert Column Rewrite,需显式选择或由特定路径使用
极稀疏更新
仍写完整 Range 的更新列
默认 Row Rewrite 或整列 Fragment Rewrite
Cell Overlay
不支持
Main 中实验性,尚非稳定 Library 能力
Nested Leaf 更新
可选,一层组合
普通 Update 不支持嵌套路径;Overlay Reader 设计可按叶子合并
BLOB
Paimon 托管 BLOB、Placeholder、Dedicated File
Lance 原生 Blob;列重写路径当前对 Blob 存在限制
Vector
Dedicated Vector File + Global Index
原生 Vector 类型与 ANN Index
Stable Row ID
Data Evolution 必须有,但维护可重分配
可选;开启后以 Row ID Sequence 跨 Rewrite 保持
删除
Deletion Vector
Deletion File/Bitmap
删除物化
重分配受影响 Row ID
可保留 Stable Row ID
SQL/引擎生态
Spark、Flink、PyPaimon
SDK、LanceDB SQL/Enterprise、DataFusion
索引更新
Throw/Drop/Ignore+Refresh
Fragment Coverage、Flat Tail、Optimize;未来 Overlay Exclusion
Compaction
合并列版本和 Range
合并/重写 Fragment、折叠删除、维护索引

20.12 Iceberg 当前的 UPDATE 模型

Iceberg V2/V3 的 Row-Level Update 主要有两类:
Copy-on-Write
优点:
  • 读取简单;
  • 最新 Snapshot 没有额外 Merge-on-Read 成本;
  • 普通 Data File 可直接使用标准 Parquet/ORC/Avro Reader。
缺点:
  • 更新一个窄列也会重写其他列;
  • 宽表、Embedding 和大型载荷写放大明显。
Merge-on-Read
典型更新可表达为:
V2 使用 Position Delete File,V3 可以使用 Deletion Vector。
它减少了立即重写原文件的成本,但新行通常仍是完整 Row,不是只含更新列的 Column File。
因此在 Iceberg V4 Column File 完整落地前:
Iceberg 有 Row-Level Update 和 Delete,但没有与 Paimon Data Evolution 等价的稳定、通用 Column Provider 读取模型。

20.13 Iceberg V3 Row Lineage

Iceberg V3 引入:
新行可以通过:
继承 Row ID。
当已有行移动到新 Data File 时,Writer 应:
  1. 复制原 _row_id;
  1. 修改行时把 _last_updated_sequence_number 写为 NULL,在提交后继承新 Sequence;
  1. 未修改行保留原 Last Updated Sequence。
这与 Paimon、Lance 的共同方向一致:
但 Iceberg Spec 特别指出:
  • Equality Delete 型更新无法保留原 Row ID;
  • 因为 Writer 不读取旧行,无法知道新行对应哪个旧 Row ID;
  • 这种更新被视为删除旧行并新增一个新身份的行。
所以 Iceberg 的 Row ID 是否稳定,还取决于 Engine 采用何种更新路径。

20.14 Iceberg V4 Column File

Iceberg V4 规范已经增加 column_files:
约束包括:
  • Column File 只能关联到 Data File 或 V4 Leaf Manifest;
  • 同一个 Field ID 在一个 Tracked File 的 Column File 列表中最多出现一次;
  • Column File 更新后,当前版本使用 MODIFIED Entry;
  • 旧元数据通过 REPLACED Entry 记录,支持 Change Detection;
  • 添加 Column File 时可以更新 Data Sequence Number;
  • Reader 需要把 Base File 和 Column File 按行位置 Stitch。
社区提案的目标非常明确:
这与 Paimon Data Evolution 和 Lance Fragment 整列替换的核心思想相同。

20.15 Iceberg V4 当前实现状态

截至本章分析的 2026-10-17:
  1. Iceberg 主规范已经描述 V4 column_files;
  1. V4 Adaptive Metadata Tree Spec PR 已合并;
  1. 主实现已支持 Format Version 4 的部分基础元数据和 Manifest 读写;
  1. 当前主分支中还没有完整、稳定的 Column File 公共模型和 Reader Stitching;
  1. 当前主分支中的兼容性元数据读写层还没有把 column_files 完整暴露为成熟的数据文件模型;
  1. Column File 相关实现仍在多个开放 PR 中推进。
社区当前已经完成或正在推进的里程碑包括:
  1. V4 Adaptive Metadata Tree 规范;
  1. Column File 的基础元数据模型;
  1. Manifest 中 Column File 的读取;
  1. Row-based Reader 的列拼接;
  1. Vectorized Reader 的列向量拼接;
  1. Generic Reader、Spark、Flink 和 Encryption 的统一接入。
因此应把 Iceberg Column File 定位为:
已进入 V4 规范、正在快速实现,但尚不能按成熟生产能力与 Paimon Data Evolution 或 Lance 整列更新直接等同。

20.16 Iceberg Proposal 与 Paimon 的关键差异

Iceberg Issue #15146 明确说明:
这意味着当前 Iceberg Column Update Proposal 更接近:
而 Paimon 已经支持:
两者都在物理上写完整对齐列文件,但操作入口不同:
场景
Paimon
Iceberg V4 Proposal
全表新列回填
支持
目标场景
全 Data File 某列刷新
支持
目标场景
Predicate 命中部分行
支持,补齐 Range 内旧值
当前 Proposal 明确列为 Non-goal
行级删除
Deletion Vector
V3/V4 Deletion Vector
稀疏 Cell Update
不支持
当前 Proposal 不支持

20.17 三者总对比

维度
Paimon Data Evolution
Lance/LanceDB
Iceberg V2/V3
Iceberg V4 Column File
当前成熟度
已实现
整列更新已实现;Overlay 实验中
行级更新成熟
规范已进入,Reader/Writer 开发中
主要更新单位
列 × Normal Range
列 × Fragment,或完整匹配行
完整行/Data File
列 × Tracked Data File
部分行部分列
支持,但写完整 Range
Column Rewrite 支持;普通 Update 默认 Row Rewrite
通过行级 MOR/COW,不是列文件
当前 Proposal 非目标
稀疏 Cell 文件
无
Data Overlay 设计
无
无
行身份
_ROW_ID
Stable _rowid 可选
V3 _row_id
继承 V3 Row Lineage
删除
DV
Deletion File
Position/Equality Delete、DV
Colocated DV
Reader 拼接
按 Row ID 位置合并 Provider
Fragment 内按位置合并 Data File
合并 Delete 与完整行
Base + Column File Stitch
大对象支持
托管 BLOB、Vector Dedicated File
Blob、Vector 原生
依赖 Data File 类型
Column File 可存标准格式列
索引
Global Index + Row Range
原生 ANN/Scalar/FTS
外部/引擎级能力为主
Secondary Index Spec 也在推进
更新后索引
Throw/Drop/Refresh
Coverage Pruning/Flat Tail/Optimize
通常重写或重新维护
实现仍在设计和开发
Compaction
合并列版本;DV 物化独立
Fragment Rewrite,可保留 Stable ID
Rewrite Data Files/Delete Files
需要整合 Column File
生态重点
Spark/Flink/Python 湖仓
ML、检索、嵌入式/服务
跨引擎湖仓标准
未来 V4 跨引擎列级更新

20.18 对 Paimon 设计的启示

启示一:Paimon 和 Lance 的成熟整列设计高度趋同
两者都选择:
这说明 Paimon 的 Row ID Range Group 并不是孤立设计,而是与 ML 数据格式的发展方向一致。
启示二:Paimon 的优势在湖仓引擎集成
Lance 在格式、随机访问、向量索引、UDF 回填和 Stable Row ID 方面更原生;Paimon 则已把列级更新统一接入:
  • Spark SQL UPDATE/MERGE;
  • Flink Procedure 和 Delete;
  • PyPaimon;
  • Catalog、Snapshot、Branch、System Table;
  • BLOB/Vector/Global Index。
对于已有 Flink/Spark 湖仓管道,Paimon 的使用门槛更低。
启示三:Lance Stable Row ID 更适合跨重写外部引用
Paimon 的 Row ID 在 DV Materialization 和显式 Reassign 时会变化。如果跨表引用 _ROW_ID 是关键需求,可以借鉴 Lance:
  • 为逻辑 ID 建立独立 Sequence;
  • 物理位置改变时保留逻辑 ID;
  • Row ID 与 Row Address 明确分离;
  • 为创建版本和最后更新版本建立行级元数据。
代价是 Row ID Sequence、映射索引和 Compaction 复杂度上升。
启示四:稀疏 Overlay 是值得关注但代价很高的方向
Paimon 当前对普通列的极稀疏更新仍有较大读取和写入放大。Lance Overlay 展示了可行方向,但也同时引入:
  • Cell Coverage;
  • 多层版本;
  • 索引 Exclusion;
  • Overlay 合并;
  • Base Fold;
  • Row Rewrite 冲突;
  • Feature Flag 和混合版本兼容。
在没有明确稀疏更新比例和收益数据前,完整 Range 列文件仍是更简单可靠的设计。
启示五:Iceberg V4 会缩小格式能力差距
Iceberg 一旦完成 Column File:
  • 宽表新列回填不再需要重写 Base File;
  • 标准 Parquet/ORC/Avro 可以作为 Column File;
  • 跨引擎兼容会成为强项;
  • Paimon 不能只依赖“支持列级更新”作为差异点。
Paimon 更需要突出:
  • 部分行匹配更新已经可用;
  • Spark/Flink/Python 统一语义;
  • BLOB Placeholder 和 Dedicated Storage;
  • Global Index 刷新;
  • Compaction Conflict Rebase;
  • 多模态端到端能力。

20.19 选型建议

需求
更适合的方向
Flink/Spark 湖仓中持续补充特征列
Paimon Data Evolution
图片/BLOB、Embedding、全文和向量检索一体化,同时仍需湖仓批处理
Paimon 或 LanceDB,取决于计算生态
Python/Rust 为主的 ML Dataset、随机访问和训练数据加载
Lance
Retrieval、Vector/FTS、低延迟 SDK 查询
LanceDB
最大化跨引擎开放格式兼容
Iceberg
当前就要生产可用的列级部分更新
Paimon 或 Lance 的整列重写路径
等待标准化 Column File 后让多引擎共同读写
关注 Iceberg V4
大量极稀疏 Cell Update
当前应基准测试 Row Rewrite 与整列 Rewrite;持续关注 Lance Overlay

20.20 延伸材料与 Roadmap

为了降低对内部代码结构的依赖,建议优先按文档主题阅读,而不是按具体文件或类名查找:
Paimon
  • Data Evolution 总览;
  • File Layout and Reads;
  • Data Evolution Maintenance;
  • PyPaimon Data Evolution;
  • Global Index 的更新与维护;
  • Lance 文件格式集成说明。
Lance 与 LanceDB
  • Lance Data Evolution;
  • Fragment 与多列文件布局;
  • Row ID and Lineage;
  • Transaction Conflict Rules;
  • Data Overlay Files;
  • LanceDB Feature Engineering、Computed Column 和 Column Refresh;
  • Update、Merge Insert、Compaction 与 Index Maintenance。
Iceberg
  • Table Format Specification 中的 Row Lineage、Deletion Vector 和 V4 Column File;
  • V4 Adaptive Metadata Tree;
  • Efficient Column Updates Proposal;
  • Column File Metadata、Manifest Reader 和 Reader Stitching 的实现进展。
Iceberg 社区 Roadmap 应以 Issue #15146 及其关联的 V4 Column File PR 为入口。具体 PR 编号和拆分方式会随开发过程变化,学习时应关注以下里程碑,而不是绑定某个内部实现名称:
  1. Column File 元数据进入公共数据模型;
  1. V4 Manifest 能够稳定读写 Column File;
  1. Row-based Reader 能按位置拼接 Base 和 Column File;
  1. Vectorized Reader 能零拷贝或低拷贝拼接列向量;
  1. Spark、Flink 和 Generic Reader 形成一致语义;
  1. Encryption、Projection、Deletion Vector 和 Change Detection 完整接入;
  1. Writer 和 Column Update API 达到可生产使用状态。

21. 关键配置表

配置
默认值
说明
row-tracking.enabled
false
为 Append Table 开启 Row ID,不可变
row-tracking.partition-group-on-commit
true
提交时按分区组织新文件,使分区内 Row ID 更连续
data-evolution.enabled
false
开启 Data Evolution,不可变
deletion-vectors.enabled
false
开启行级逻辑删除
data-evolution.write-cols-optimization.enabled
false
完整非 Dedicated 列文件省略 writeCols
data-evolution.nested-field.enabled
false
开启 Struct 子字段级更新
data-evolution.row-id-conflict-rewrite.max-size
256 MB
PyPaimon 边界冲突自动重写上限
data-evolution.row-sidecar.enabled
false
生成稀疏 Row ID 读取 Sidecar
data-evolution.row-sidecar.max-selected-rows
4096
Sidecar 最大选中行数
data-evolution.row-sidecar.max-selection-ratio
0.05
Sidecar 最大选中比例
data-evolution.merge-into.file-pruning
true
Spark MERGE 部分列写前裁剪未触碰文件
data-evolution.merge-into.source-persist
false
Data Evolution Merge 是否缓存 Source
data-evolution.compaction.split-large-files
false
Compaction 是否拆分大 Normal File
data-evolution.compaction.large-file-ratio
2.0
大文件判定倍率
blob-compaction.enabled
false
是否执行 BLOB Compaction
global-index.column-update-action
THROW_ERROR
更新索引列时的行为
Spark Connector 专用配置:
配置
默认值
spark.paimon.write.data-evolution.update-conflict-retry.max-attempts
20
spark.paimon.write.data-evolution.update-conflict-retry.wait-ms
10

22. 引擎能力对比

能力
Spark
Flink
PyPaimon
INSERT 新行
支持
支持
支持
UPDATE 普通列
支持
通过专用 procedure
支持
MERGE Matched Update
支持
支持
支持
MERGE Not Matched Insert
支持
专用 procedure 不支持
支持
MERGE Matched Delete
支持,需 DV
专用 procedure 不支持
支持,需 DV
Not Matched By Source Delete
Spark 3.4+,需 DV
专用 procedure 不支持
视 API 规则
DELETE FROM
支持,需 DV
Flink 1.17+ Batch,需 DV
delete_by_predicate
按 _ROW_ID 更新
内部和 SQL 快捷路径
内部支持
直接 API
Shard 派生列回填
可通过 Spark SQL/DF
可通过任务实现
原生支持
普通 Compaction
支持
支持
依赖相应维护能力
Materialize DV
支持
支持
依赖相应维护能力

23. 观测与排障

23.1 查看逻辑行和 Row ID

23.2 查看当前文件布局

检查要点:
  1. Normal File Range 是否相同或不相交;
  1. 相同 first_row_id 的 Normal File 是否有相同 record_count;
  1. Dedicated File 是否完整包含于一个 Normal Range;
  1. write_cols 是否符合预期;
  1. 同一列是否存在过多版本;
  1. 文件数是否因频繁更新持续增长。

23.3 不要用 $files.record_count 求逻辑行数

多个列文件可能代表相同 Row ID Range:
逻辑行数仍可能只有 100,而不是 300。
此外,Deletion Vector 还会减少可见行数。应查询表本身获取逻辑行数。

23.4 常见问题

更新一行为什么写了很多行?
因为普通列更新单位是“选中列 × 完整 Normal Range”,不是单个匹配行。
UPDATE 后读取文件越来越多?
每次部分列更新会增加列版本,需要定期 compaction。
同一文件范围内更新不同业务行为什么冲突?
两个 Writer 如果都更新相同列,就都在替换该列的完整 Range。
新增列后为什么历史行是 NULL?
Schema Evolution 只新增列定义,不自动执行数据回填。
删除后文件大小为什么没有下降?
普通删除只写 Deletion Vector。需要 materialize_deletion_vectors 才会移除位置,但 Row ID 会改变。
Compaction 后更新提交为什么失败?
可能是 Normal Range 边界变化导致 Layout Conflict。重新规划,或使用支持 Rebase 的 Spark/PyPaimon 路径。
更新索引列为什么报错?
默认 global-index.column-update-action=THROW_ERROR,需要选择删除分区索引、忽略后重建,或先手动删除索引。

24. 文件大小设计

target-file-size 不只是影响扫描性能,也影响未来的更新成本。

大文件

优点:
  • 全表扫描文件数少;
  • Manifest 和文件打开开销低。
缺点:
  • 更新一行也要处理更大的 Normal Range;
  • 单次部分列更新成本高;
  • 并发更新更容易落入同一个 Range。

小文件

优点:
  • 更新粒度更小;
  • 冲突域更小;
  • 稀疏更新成本低。
缺点:
  • 文件数多;
  • 扫描和元数据开销高;
  • compaction 压力更大。
可同时考虑:
target-file-row-num 只约束支持该选项的新写入,不会在普通部分更新时拆开已有 Normal Range。Data Evolution compaction 也可能产生更大的合并范围。

25. 已知限制和使用边界

  1. 不能定义 Primary Key;
  1. 只支持 bucket = -1;
  1. 不能开启 clustering.incremental;
  1. Matched partial update 不能修改分区列;
  1. 普通列的稀疏更新仍要处理完整文件范围;
  1. Dedicated Vector File Compaction 尚未支持;
  1. 包含 Dedicated Vector File 的组不能 Materialize DV;
  1. data-evolution.compaction.rewrite-row-ids=true 已废弃并会被拒绝;
  1. Branch Merge 不支持 Data Evolution 表;
  1. import_files 不支持 Row Tracking 或 Data Evolution 表;
  1. Nested Field 开启后存在严格的混合版本和降级风险;
  1. _ROW_ID 不能作为永久业务标识;
  1. 更新已建立 Global Index 的列必须处理索引新鲜度;
  1. 并发 Delete、Append、Compact、Overwrite 可能发生提交冲突,应做好任务编排和重试;
  1. 历史 Snapshot/Tag 会延迟旧文件的空间回收。

26. 推荐实践

26.1 表设计

  • 保留稳定的业务 ID,但不要把它误认为 Paimon Primary Key;
  • 根据更新粒度设计 target-file-size;
  • 大 BLOB、Vector 使用 Dedicated Storage;
  • 需要行级删除时,从建表阶段就规划 Deletion Vector;
  • 对长期保存的 _ROW_ID 引用建立维护协议。

26.2 更新任务

  • 只声明真正需要更新的列;
  • 避免 UPDATE SET 中无意义地重写全部列;
  • 尽量按分区限制更新范围;
  • 对大范围更新评估是否关闭 MERGE File Pruning;
  • 对确定性 Spark UPDATE 配置合理冲突重试;
  • 对频繁更新的列设置定期 compaction。

26.3 索引

  • 更新索引列前明确选择 global-index.column-update-action;
  • IGNORE 后必须安排索引刷新或重建;
  • 用 Row Sidecar 优化全局索引后的稀疏 Row ID 回表;
  • DV Materialization 后重新构建受影响索引。

26.4 运维

  • 定期查询 $files 检查列版本数量;
  • 监控小文件数量、单 Range 文件数量和读取放大;
  • 普通 compaction 与 DV materialization 分开调度;
  • Materialize DV 前确认下游没有长期依赖旧 _ROW_ID;
  • 结合 Snapshot/Tag 保留策略安排空间回收。

27. 实现原理核对路线

不建议从具体类名开始阅读,因为内部结构会随重构变化。更稳定的方法是按问题和模块逐层核对。

第一阶段:表模型与配置

重点理解:
  • 为什么必须无主键、bucket=-1;
  • Data Evolution 如何改变普通 Append Table 的扫描和读取语义;
  • 哪些选项不可变。

第二阶段:Row ID 分配

重点理解:
  • nextRowId;
  • firstRowId;
  • Append、Partial Update、Compact 文件的不同处理。

第三阶段:文件扫描与读取

重点理解:
  • Row Range Group;
  • Provider 选择;
  • Projection Pruning;
  • Deletion Vector;
  • Nested Field;
  • BLOB Fallback;
  • Row Sidecar。

第四阶段:Spark 写入

重点理解:
  • UPDATE 如何转换成 Self Merge;
  • _ROW_ID 快捷路径;
  • 完整 Range 补齐;
  • File Pruning;
  • Compaction Conflict Rebase。

第五阶段:Flink 写入

重点理解:
  • 固定 Snapshot;
  • 按 firstRowId Shuffle;
  • _ROW_ID 排序;
  • 原值补齐;
  • BLOB Placeholder;
  • Deletion Vector 写入。

第六阶段:冲突和维护

  • 找到提交冲突检测模块,核对 Range 与 Field ID 的冲突条件;
  • 找到普通 Compaction 规划和执行模块,核对 Row ID 保留规则;
  • 找到 Deletion Vector Materialization,核对物理删除和 Row ID 重分配;
  • 找到边界变化后的重试或 Rebase 路径,核对其适用条件。

第七阶段:索引

  • 核对索引文件如何记录 Row Range 和源 Snapshot;
  • 核对更新索引列后的 Throw、Drop 或 Ignore 行为;
  • 核对未覆盖范围与已覆盖但可能过期范围的区别;
  • 核对增量刷新如何识别字段版本变化。

28. 推荐验证场景

比记住测试类名更重要的是理解每组测试验证的契约。

Core 语义

  • 同一 Row ID Range 的多列文件能正确合并;
  • 缺失列、显式 NULL 和 BLOB Placeholder 的语义不同;
  • Projection Pruning 不会丢失行数或 Deletion Vector Anchor;
  • 普通文件索引和 Bitmap Selection 不破坏列位置对齐;
  • Nested Field Provider 能按支持的层级正确组合;
  • 非法 Normal/Dedicated Range 会在提交或读取时被拒绝。

并发与提交

  • 同一 Range 更新不同字段可以提交;
  • 同一 Range 更新相同字段发生冲突;
  • Compaction 改变边界后能够 Rebase,或给出明确冲突;
  • Overwrite、分区删除和 Row ID 重分配不会被旧计划静默覆盖;
  • Deletion Vector 与部分列更新的并发行为符合预期。

Spark 与 Flink

  • UPDATE 和 MERGE 只写声明更新的字段;
  • 未命中行在输出列文件中得到正确旧值;
  • _ROW_ID 快捷路径和普通业务条件路径结果一致;
  • DELETE 正确生成并应用 Deletion Vector;
  • 分区裁剪、文件裁剪和 Self Merge 优化不改变结果。

Python

  • Predicate、Row ID 和 Shard Update 结果一致;
  • Shard Update 对行数和顺序进行严格校验;
  • Stream Commit Identifier 生命周期正确;
  • Native Commit、REST Commit 和普通 Catalog 行为一致;
  • Row Sidecar 只在稀疏读取阈值内启用。

29. 建议动手实验

实验一:观察一次部分列更新

  1. 建 Data Evolution 表;
  1. 插入 5 行;
  1. 查询 $row_tracking;
  1. 查询 $files;
  1. 只更新其中一行的一个列;
  1. 再次查询 $files;
  1. 验证新文件与原 Normal File 的 Row Range 完全相同。

实验二:验证不同列并发更新模型

  1. 两个任务固定同一个 Snapshot;
  1. 一个更新 b,一个更新 c;
  1. 对比更新相同列 b 的行为;
  1. 对照“Range 相交且字段集合相交”的冲突模型解释结果。

实验三:删除与物化

  1. 开启 Deletion Vector;
  1. 删除一行;
  1. 验证 Row ID Range 和 record_count 不变;
  1. 执行普通 compaction,确认 Row ID 不变;
  1. 执行 materialize_deletion_vectors;
  1. 观察新 Row ID 和索引变化。

实验四:Compaction 边界变化

  1. 创建两个相邻 Normal Range;
  1. 固定旧 Snapshot 规划更新;
  1. 运行 compaction 合并范围;
  1. 提交旧更新;
  1. 观察 Layout Conflict 或 Rebase。

实验五:Row Sidecar

  1. 开启 data-evolution.row-sidecar.enabled;
  1. 构建标量 Global Index;
  1. 查询少量 Row ID;
  1. 对比 Sidecar 开关下读取的物理文件和性能。

30. 最终心智模型

可以把 Data Evolution 看成一个以 Row ID Range 为单位的、支持多版本列文件的存储模型:
读取时:
更新时:
维护时:
只要始终抓住三个关键词,就能理解大部分实现:
  1. Row ID 对齐;
  1. 列 Provider 最新版本胜出;
  1. Normal Range 必须完全相同或完全不相交。

31. 延伸阅读

建议优先阅读以下稳定主题:
  1. Data Evolution:建表、更新、删除和并发行为;
  1. File Layout and Reads:Row ID Range、Provider 选择和文件契约;
  1. Data Evolution Maintenance:普通 Compaction、文件拆分和 DV Materialization;
  1. PyPaimon Data Evolution:Predicate、Row ID、Shard 和 Stream Update;
  1. Global Index Management:索引覆盖、新鲜度和更新策略;
  1. Flink Action 与 Procedure:Merge、Delete 和维护任务;
  1. Spark Maintenance 与 Index Procedure:Compaction、DV 和 Global Index;
  1. Lance Data Evolution 与 Row Lineage:Fragment、Column Replacement 和 Stable Row ID;
  1. Lance Data Overlay:稀疏 Cell 更新的实验性设计;
  1. Iceberg Table Format Specification:V3 Row Lineage、Deletion Vector 和 V4 Column File;
  1. Iceberg Efficient Column Updates Proposal:Column File 的目标、非目标和实施进展。
阅读在线文档时应注意版本选择。master 或 main 页面反映正在开发的行为,未必代表某个已发布版本已经具备相同能力。