湖仓——第一阶段:元数据目录

💡 原文英文,约1900词,阅读约需7分钟。
📝

内容提要

本文介绍用Go构建小型湖仓引擎,模仿DuckLake架构:元数据存于Postgres,数据用Parquet。核心是八张元数据表,涵盖快照、表、列、数据文件、列统计、删除文件等。通过begin_snapshot和end_snapshot实现快照可见性与时间旅行。相比Iceberg的清单文件树,数据库方案让文件裁剪变成SQL查询,原子提交只需事务,但依赖运行中的数据库。

🔎

延伸解读

数据库目录的代价与适用场景

将元数据存入Postgres虽然简化了文件裁剪和原子提交,但引入了对运行中数据库实例的依赖。Iceberg仅靠文件系统即可运作,而DuckLake方案必须保证数据库可用。因此,这种架构更适合已有稳定数据库基础设施、且愿意用运维复杂度换取查询简洁性的场景;若追求极简部署或离线环境,清单文件方案可能更合适。

快照可见性过滤器的统一逻辑

所有元数据查询都复用同一个可见性条件:begin_snapshot <= N 且 end_snapshot 为空或大于 N。这使时间旅行无需额外机制,只需改变N值。删除表或列时并不物理删除行,而是设置end_snapshot,旧快照仍可访问历史数据。这种设计让元数据天然支持版本回溯,但也意味着元数据表会随快照增长而累积历史行。

字段ID:模式演化的关键

每个列在创建时分配永久field_id,即使列被重命名,field_id也不变。旧名称行设置end_snapshot,新名称行沿用同一field_id并赋予新的begin_snapshot。Parquet文件将field_id写入模式元数据,读取时据此映射列。这保证了重命名或类型调整不会破坏已有数据文件的解析,是模式演化可靠性的基础。

扫描规划:从文件树到SQL查询

Iceberg扫描需多次网络获取并解析Avro清单文件,而DuckLake将文件裁剪转化为单条SQL:连接数据文件表与列统计表,用WHERE过滤min/max值。这减少了网络往返和解析开销,但把复杂度转移到了数据库查询优化上。若统计信息不准确或缺失,裁剪效果会打折扣,因此维护列统计的完整性至关重要。

❓

Q&A

湖仓的元数据层为什么重要?它和数据文件有什么区别?

湖仓有两层:数据文件(包含实际行的 Parquet 文件)和元数据(哪些文件存在、有哪些列、每个文件包含的值范围)。元数据层是湖仓与一堆 Parquet 文件的区别所在。

DuckLake 把元数据存在关系数据库里,和 Iceberg 的清单文件树相比有什么不同?

Iceberg 将元数据存储为对象存储上的清单文件树(JSON/Avro),每次查询都要下载并解析这条文件链。DuckLake 则把整个清单树替换为关系数据库中的表,“该扫描哪些文件”变成带 JOIN 和 WHERE 的 SQL 查询,数据库原生处理索引、并发和原子提交。

用数据库存元数据会带来哪些具体好处和代价?

好处:文件裁剪变成 SQL 查询(如 WHERE max_value >= '100');原子提交只需 BEGIN 和 COMMIT;模式历史可查询(如查快照 5 有哪些列)。代价:依赖一个运行中的数据库实例,而 Iceberg 仅靠文件系统就能运行。

湖仓元数据数据库包含哪八张表?各自作用是什么?

八张表:ducklake_snapshot(版本历史)、ducklake_table(表定义,带快照可见性)、ducklake_column(列定义,带永久字段 ID)、ducklake_data_file(Parquet 文件注册表)、ducklake_file_column_stats(每文件每列的最小/最大值和空值数)、ducklake_delete_file(位置删除文件)、ducklake_schema(命名空间)、ducklake_snapshot_changes(变更描述)。

快照可见性过滤是如何实现时间旅行的?

几乎每个元数据查询都包含过滤条件:WHERE begin_snapshot <= $1 AND (end_snapshot IS NULL OR end_snapshot > $1)。每行元数据都有由 begin_snapshot(创建时)和 end_snapshot(退役时,NULL 表示仍活跃)定义的生命周期。在快照 N 查询时,只看到生命周期包含 N 的行,这就是时间旅行的机制。

删除表时元数据行会被删除吗?数据还能访问吗?

不会。删除表只是在该表行、其列和数据文件上设置 end_snapshot,元数据行从不删除。数据仍可通过时间旅行在更早的快照中访问。

Postgres 为元数据提供了哪些 ACID 特性?

原子性:写入 Parquet 文件并在目录中注册的 INSERT 在单个 Postgres 事务中完成,失败则整体回滚。一致性:NOT NULL 约束防止无效元数据状态。隔离性:并发创建快照的事务由 Postgres 串行化。持久性:COMMIT 返回后元数据持久化到 Postgres 的 WAL。

🏷️

标签

➡️

继续阅读