湖仓项目第二阶段:Parquet数据文件

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

内容提要

湖仓项目第二阶段使用Go语言的parquet-go库读写Parquet文件。Parquet为列式存储,记录每列min、max、null_count统计信息,支持文件裁剪以减少I/O。写入时通过StatsCollector实时收集统计;读取时按列投影,仅读所需列。每列带field ID,重命名后仍能正确匹配,为模式演进奠定基础。文件以UUID命名避免冲突。

🔎

延伸解读

列式存储与文件裁剪的协同效应

文章指出,Parquet的列式布局让查询只需读取涉及的列,而每列的min/max统计信息进一步支持文件裁剪。两者结合可大幅减少I/O:例如20列中查2列,再跳过7/10的文件,实际读取量仅为总数据的1.5%。这种协同是湖仓查询性能的关键,也解释了为何统计信息必须随写入实时收集。

Field ID:模式演进的基石

每个Parquet列携带与catalog FieldID一致的整数ID,读取时按ID而非列名匹配。这样即使列被重命名,旧文件中的数据仍能正确对应到新列名。文章以amount重命名为total为例,说明该机制为后续模式演进阶段铺平了道路,避免了因重命名导致的数据错位。

写入时统计收集的设计取舍

Go实现使用StatsCollector在写入过程中实时更新每列的min、max和null_count,而非事后解析文件元数据。min/max被序列化为字符串存入catalog,这与DuckLake的C++实现一致,目的是避免统计表出现类型化列。这种设计简化了catalog结构,但比较时需依赖逻辑类型进行正确排序。

UUID命名与并发写入

数据文件采用ducklake-{uuid}.parquet格式命名,每次INSERT生成一个新文件。UUID保证并发写入时不会产生路径冲突,无需额外协调。这种简单策略适合湖仓的不可变文件模型,但也意味着小文件可能增多,后续阶段需考虑合并或优化。

❓

Q&A

为什么湖仓项目选择用Parquet格式存储数据文件?

Parquet是列式存储格式,查询时只需读取用到的列,结合每列的min、max、null_count统计信息,可以跳过不匹配的文件和行组,大幅减少I/O。同时列式布局让相似值集中,压缩率可达5-10倍。Parquet文件自带schema和统计信息,任何兼容工具都能直接读取。

Parquet文件中的列统计信息是如何收集的?

在写入时通过StatsCollector实时收集。每写入一个值,Observe方法会更新对应列的min、max和null_count。min/max值最终序列化为字符串存储到元数据目录中,避免在统计表中使用带类型的列。

Parquet的列统计信息如何帮助文件裁剪?

每个列块记录min_value、max_value和null_count。查询时,扫描引擎根据WHERE条件判断:如果某文件的max值小于查询阈值,或min值大于阈值,则整个文件可以跳过。例如查询price > 500,若文件max=200则跳过,若min=600则所有行匹配。

Parquet文件中的field ID有什么作用?

每个列在Parquet schema中携带一个整数field ID,与元数据目录中的FieldID对应。读取时按field ID匹配列,而不是按列名。这样即使列被重命名,旧文件中的field ID仍能正确匹配到目录中的新列名,为模式演进奠定基础。

湖仓项目如何命名Parquet数据文件?

数据文件使用UUID命名,路径格式为{schema_name}/{table_name}/data/ducklake-{uuid}.parquet。每次INSERT创建一个新文件,UUID确保并发写入时不会发生文件名冲突,无需额外协调。

读取Parquet文件时如何实现列投影?

读取时传入projectedColumns列表,构建投影集合。遍历行组时,只读取投影集合中的列,其他列的数据不会被读取、解压或分配内存。例如SELECT name FROM users只读取name列,跳过其他所有列块。

🏷️

标签

➡️

继续阅读