湖仓 – 第七阶段:删除与更新

💡 原文英文,约1500词,阅读约需6分钟。
📝

内容提要

本文介绍湖仓表的删除与更新机制。由于Parquet文件不可变,系统采用位置删除文件记录被逻辑删除的行位置,扫描时跳过这些行。删除策略分为写时复制与读时合并,DuckLake采用后者。DELETE操作先扫描匹配行位置,再写入删除文件并登记元数据;UPDATE在同一事务中拆分为DELETE加INSERT以保证原子性。扫描时加载删除位置并过滤。该方案避免重写大文件,代价是读取开销增加,可通过压缩定期合并缓解。

🔎

延伸解读

写时复制与读时合并的取舍

文章对比了两种删除策略:写时复制(COW)直接重写整个数据文件,读取简单但写入昂贵,Delta Lake 默认采用;读时合并(MOR)只写一个小删除文件记录位置,写入便宜但扫描时需额外过滤,Iceberg 和 DuckLake 采用。选择取决于读写负载:频繁删除且文件巨大时,MOR 避免重写多 GB 文件;若读取延迟敏感且删除较少,COW 可能更简单。

删除文件的结构与元数据追踪

DuckLake 的位置删除文件是标准 Parquet,仅含 data_file_id 和 position 两列,按数据文件分组记录被逻辑删除行的从 0 开始的位置。这些文件登记在 ducklake_delete_file 表中,与数据文件共享快照可见性,因此删除操作只需新增小文件并提交快照,原始数据文件在磁盘上保持不变。这种设计让删除的写入成本与匹配行数成正比,而非文件大小。

UPDATE 的原子性如何保证

UPDATE 被拆分为同一事务内的 DELETE 加 INSERT:先扫描定位旧行并写删除文件,再写入包含更新值的新数据文件,最后在单个事务中同时登记删除文件、新数据文件和快照。由于两者共享同一快照,读者要么看到更新前的旧行,要么看到更新后的新行,不会出现中间状态。这依赖事务机制将多步操作原子提交,而非依赖文件系统层面的原子替换。

读放大与压缩的平衡

读时合并的代价是扫描时需加载删除位置并逐行过滤,文章用 map 实现 O(1) 查找,并指出删除很多时 bitset 更省内存(一万行约 1.2 KB)。实际开销通常较小,因为多数行未被删除,且位置检查很快。但删除文件会随删除操作累积,文章建议通过压缩定期重写文件来吸收已积累的删除,从而控制读放大,这需要额外的维护策略。

❓

Q&A

湖仓表为什么需要位置删除文件?

因为Parquet文件不可变,写入后无法直接删除某一行而不重写整个文件。位置删除文件记录被逻辑删除的行位置,扫描时跳过这些行,从而避免重写大文件。

Copy-on-Write和Merge-on-Read两种删除策略有什么区别?

Copy-on-Write重写整个数据文件并省略删除行,读取简单但写入昂贵,Delta Lake默认采用;Merge-on-Read写一个小删除文件列出删除位置,写入便宜但读取稍贵,Iceberg和DuckLake采用。

DuckLake的删除文件格式是什么样的?

删除文件是Parquet文件,包含两列:data_file_id(BIGINT,标识数据文件)和position(BIGINT,0-based行号),用于标识特定数据文件中被删除的行。

DuckLake执行DELETE语句的步骤是什么?

先扫描匹配行并记录文件位置,然后为每个数据文件写入删除文件,接着在元数据中登记删除文件并创建新快照,最后返回删除行数。数据文件本身不变。

UPDATE操作在DuckLake中是如何保证原子性的?

UPDATE被拆分为DELETE加INSERT,并在同一个事务中完成:先写删除文件标记旧行,再写新数据文件包含更新后的行,两者共享同一快照,因此读者看到原子更新。

使用位置删除文件后,扫描时如何过滤已删除的行?

扫描每个数据文件时,查询关联的删除文件,将删除位置加载到map中,读取行时跳过这些位置。对于删除较多的文件,可用bitset更节省内存。

位置删除方案的主要代价是什么?如何缓解?

主要代价是读取放大,因为扫描时需要加载删除位置并过滤。但大多数行未被删除,开销较小;可通过压缩定期重写文件来吸收累积的删除。

🏷️

标签

➡️

继续阅读