1. 一句话理解 Data Evolution
Paimon Data Evolution 是一种面向 无主键 Append Table 的列级更新机制:
- 每一行由 Paimon 分配稳定的
_ROW_ID;
- 更新时只写发生变化的列,不重写未变化的列或大对象;
- 同一逻辑行的不同列可以来自不同物理文件;
- 读取时按
_ROW_ID对齐,并按文件版本为每一列选择最新值;
- 多次更新产生的列文件最终通过 compaction 合并。
它特别适合如下流水线:
- 先写入原始数据;
- 后续异步补充标签、描述、特征、Embedding 等列;
- 原始图片、音视频等大对象保持不动;
- 持续构建或刷新向量、全文、标量索引。
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 表必须满足:
- 是 Append Table;
- 没有 Primary Key;
bucket = -1,即 bucket-unaware;
- 必须启用 Row Tracking;
- 不能启用
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 会:
- 按重叠 Row ID Range 对文件分组;
- 合并最新列 Provider 的统计信息;
- 基于逻辑最新值做文件组级过滤;
- 根据 Projection 和 Predicate 需要,删除不贡献列的物理文件;
- 必要时保留 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:
- 选中行数不超过 4096;
- 选中行比例不超过 5%;
- 不是全文件读取;
- 文件存在唯一的 Row Sidecar;
- 不是 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;
- 输入按
firstRowIdShuffle,并按_ROW_ID排序;
- 每个范围补齐未更新行后写部分列文件。
删除应使用:
- Flink 1.17+ Batch SQL
DELETE FROM;或者
deleteAction。
12. PyPaimon 更新方式
PyPaimon 提供多种 API。
12.1 按 Predicate 更新
12.2 按 Row ID 更新
输入只需要
_ROW_ID 和待更新列:12.3 Shard Update
适合批量计算派生列:
必须遵守:
- 输出批次与输入批次行数相同;
- 顺序相同;
- 不能过滤、排序、复制行;
- 不同 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 升降级风险
该选项具有明显的混合版本风险:
- 必须先升级所有 Reader、Writer、Compactor 和维护任务;
- 只能通过持久化表选项开启;
- 开启后不能关闭或删除;
- 一旦提交了嵌套路径文件,旧版本 Reader 无法正确重建;
- 二进制降级不安全。
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
当需要真正移除删除位置时,运行:
该过程会:
- 读取受影响文件组;
- 应用 Deletion Vector;
- 只写存活行;
- 为存活行分配新 Row ID;
- 删除已物化的 Deletion Vector;
- 删除受影响的 Global Index;
- 对受影响 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 可能重写整行,甚至重新处理大型图片。列级更新希望把写入缩小为:三者的共同思想是:
- 逻辑行不再要求全部列位于一个物理文件;
- 数据文件需要某种稳定的位置或行身份;
- Reader 负责把多个物理来源拼成逻辑行;
- 更新文件积累后需要 Compaction;
- 更新过的索引不能继续无条件使用;
- 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”和“新的列文件”作为一个原子替换单元提交。
要求:
- 目标字段必须已经存在于 Dataset Schema;
- 当前 API 按顶层列整体替换,不能只暂存一个嵌套叶子字段;
- 输入必须产生与 Fragment
physical_rows完全相同的行数;
- 输入包含被删除的物理位置,不能先应用 Deletion File 后缩短;
- 新文件与 Fragment 按物理位置对齐;
- 调用方需要基于正确 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。
其执行方式是:
- 找出匹配行;
- 将旧物理行加入 Deletion File;
- 写出包含更新后完整行的新 Fragment;
- Stable Row ID 开启时把原 Row ID 复制给新物理行;
- 新地址替换旧地址。
因此不能笼统地说:
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:
- 从索引结果排除 Overlay 更新过的位置;
- 用当前值重新扫描和判断这些位置;
- 将仍然匹配的行补回结果。
相比之下:
设计 | 优点 | 代价 |
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 应:
- 复制原
_row_id;
- 修改行时把
_last_updated_sequence_number写为NULL,在提交后继承新 Sequence;
- 未修改行保留原 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 更新后,当前版本使用
MODIFIEDEntry;
- 旧元数据通过
REPLACEDEntry 记录,支持 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:
- Iceberg 主规范已经描述 V4
column_files;
- V4 Adaptive Metadata Tree Spec PR 已合并;
- 主实现已支持 Format Version 4 的部分基础元数据和 Manifest 读写;
- 当前主分支中还没有完整、稳定的 Column File 公共模型和 Reader Stitching;
- 当前主分支中的兼容性元数据读写层还没有把
column_files完整暴露为成熟的数据文件模型;
- Column File 相关实现仍在多个开放 PR 中推进。
社区当前已经完成或正在推进的里程碑包括:
- V4 Adaptive Metadata Tree 规范;
- Column File 的基础元数据模型;
- Manifest 中 Column File 的读取;
- Row-based Reader 的列拼接;
- Vectorized Reader 的列向量拼接;
- 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 编号和拆分方式会随开发过程变化,学习时应关注以下里程碑,而不是绑定某个内部实现名称:- Column File 元数据进入公共数据模型;
- V4 Manifest 能够稳定读写 Column File;
- Row-based Reader 能按位置拼接 Base 和 Column File;
- Vectorized Reader 能零拷贝或低拷贝拼接列向量;
- Spark、Flink 和 Generic Reader 形成一致语义;
- Encryption、Projection、Deletion Vector 和 Change Detection 完整接入;
- 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 查看当前文件布局
检查要点:
- Normal File Range 是否相同或不相交;
- 相同
first_row_id的 Normal File 是否有相同record_count;
- Dedicated File 是否完整包含于一个 Normal Range;
write_cols是否符合预期;
- 同一列是否存在过多版本;
- 文件数是否因频繁更新持续增长。
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. 已知限制和使用边界
- 不能定义 Primary Key;
- 只支持
bucket = -1;
- 不能开启
clustering.incremental;
- Matched partial update 不能修改分区列;
- 普通列的稀疏更新仍要处理完整文件范围;
- Dedicated Vector File Compaction 尚未支持;
- 包含 Dedicated Vector File 的组不能 Materialize DV;
data-evolution.compaction.rewrite-row-ids=true已废弃并会被拒绝;
- Branch Merge 不支持 Data Evolution 表;
import_files不支持 Row Tracking 或 Data Evolution 表;
- Nested Field 开启后存在严格的混合版本和降级风险;
_ROW_ID不能作为永久业务标识;
- 更新已建立 Global Index 的列必须处理索引新鲜度;
- 并发 Delete、Append、Compact、Overwrite 可能发生提交冲突,应做好任务编排和重试;
- 历史 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;
- 按
firstRowIdShuffle;
_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. 建议动手实验
实验一:观察一次部分列更新
- 建 Data Evolution 表;
- 插入 5 行;
- 查询
$row_tracking;
- 查询
$files;
- 只更新其中一行的一个列;
- 再次查询
$files;
- 验证新文件与原 Normal File 的 Row Range 完全相同。
实验二:验证不同列并发更新模型
- 两个任务固定同一个 Snapshot;
- 一个更新
b,一个更新c;
- 对比更新相同列
b的行为;
- 对照“Range 相交且字段集合相交”的冲突模型解释结果。
实验三:删除与物化
- 开启 Deletion Vector;
- 删除一行;
- 验证 Row ID Range 和
record_count不变;
- 执行普通 compaction,确认 Row ID 不变;
- 执行
materialize_deletion_vectors;
- 观察新 Row ID 和索引变化。
实验四:Compaction 边界变化
- 创建两个相邻 Normal Range;
- 固定旧 Snapshot 规划更新;
- 运行 compaction 合并范围;
- 提交旧更新;
- 观察 Layout Conflict 或 Rebase。
实验五:Row Sidecar
- 开启
data-evolution.row-sidecar.enabled;
- 构建标量 Global Index;
- 查询少量 Row ID;
- 对比 Sidecar 开关下读取的物理文件和性能。
30. 最终心智模型
可以把 Data Evolution 看成一个以 Row ID Range 为单位的、支持多版本列文件的存储模型:
读取时:
更新时:
维护时:
只要始终抓住三个关键词,就能理解大部分实现:
- Row ID 对齐;
- 列 Provider 最新版本胜出;
- Normal Range 必须完全相同或完全不相交。
31. 延伸阅读
建议优先阅读以下稳定主题:
- Data Evolution:建表、更新、删除和并发行为;
- File Layout and Reads:Row ID Range、Provider 选择和文件契约;
- Data Evolution Maintenance:普通 Compaction、文件拆分和 DV Materialization;
- PyPaimon Data Evolution:Predicate、Row ID、Shard 和 Stream Update;
- Global Index Management:索引覆盖、新鲜度和更新策略;
- Flink Action 与 Procedure:Merge、Delete 和维护任务;
- Spark Maintenance 与 Index Procedure:Compaction、DV 和 Global Index;
- Lance Data Evolution 与 Row Lineage:Fragment、Column Replacement 和 Stable Row ID;
- Lance Data Overlay:稀疏 Cell 更新的实验性设计;
- Iceberg Table Format Specification:V3 Row Lineage、Deletion Vector 和 V4 Column File;
- Iceberg Efficient Column Updates Proposal:Column File 的目标、非目标和实施进展。
阅读在线文档时应注意版本选择。
master 或 main 页面反映正在开发的行为,未必代表某个已发布版本已经具备相同能力。