Arrow 和 Parquet 第 1 部分:原始类型和可空性
发布日期 2022 年 10 月 05 日
作者: tustvold 和 alamb
简介
我们最近完成了 Rust Apache Arrow 中的一个长期项目,以完成对任意嵌套 Parquet 和 Arrow 模式的读写支持。这是一个复杂的话题,我们发现缺乏易于理解的技术信息,因此撰写了这篇博客与社区分享我们的经验。
Apache Arrow 是一种开放的、与语言无关的列式内存格式,用于平面和分层数据,旨在实现高效的分析操作。Apache Parquet 是一种开放的、面向列的数据文件格式,旨在实现非常高效的数据编码和检索。
分析系统越来越多地使用 Arrow 来处理存储在 Parquet 文件中的数据,因此它们之间快速、高效和正确的转换是关键的构建块。
历史上,分析处理主要侧重于查询具有表格模式的数据,其中列数固定,每行包含每个列的单个值。然而,随着 XML、JSON 等结构化文档格式的日益普及,仅支持表格模式可能会让用户感到沮丧,因为它需要通常不平凡的数据转换来首先展平文档数据。
自 2022 年 8 月发布的 20.0.0 版本起,Rust Arrow 对结构化类型的读取实现功能已完善。入门说明可以在此处找到,如有任何问题,请随时在我们的错误跟踪器上提出。
在本系列中,我们将解释 Parquet 和 Arrow 如何表示嵌套数据,突出它们之间的异同,并介绍在格式之间转换的实用性。
列式 vs 面向记录
首先,有必要退一步讨论列式和面向记录数据格式之间的区别。在面向记录的数据格式(例如换行符分隔的 JSON (NDJSON))中,给定记录的所有值都连续存储。
例如:
{"Column1": 1, "Column2": 2}
{"Column1": 3, "Column2": 4, "Column3": 5}
{"Column1": 5, "Column2": 4, "Column3": 5}
在列式表示中,给定列的数据改为连续存储
Column1: [1, 3, 5]
Column2: [2, 4, 4]
Column3: [null, 5, 5]
除了可能产生更好的数据压缩外,列式布局还可以显著提高某些查询的性能。这是因为将数据连续地布局在内存中允许编译器和 CPU 更好地利用并行性。 SIMD 和 ILP 的具体细节远远超出了本文的范围,但重要的启示是,处理大量数据块而没有干预条件分支具有显著的性能优势。
Parquet vs Arrow
Parquet 和 Arrow 是互补技术,它们在设计上做出了一些不同的权衡。特别是,Parquet 是一种存储格式,旨在实现最大的空间效率,而 Arrow 是一种内存格式,旨在由向量化计算内核操作。
主要区别在于 Arrow 为任何数组索引提供 O(1) 随机访问查找,而 Parquet 不提供。特别是,Parquet 使用 dremel 记录拆分、变长编码方案和 块压缩 来大幅减小数据大小,但这些技术会损失高性能的随机访问查找。
一种发挥每种技术优势的常见模式是,将数据从压缩表示(例如 Parquet)以 Arrow 格式的千行批次流式传输,单独处理这些批次,并将结果累积到更压缩的表示中。这受益于能够高效地对 Arrow 数据执行计算,同时控制内存需求,并允许计算内核与源和目标的编码无关。
Arrow 主要是一种内存格式,而 Parquet 是一种存储格式。
不可空原始列
让我们从最简单的不可为空的 32 位有符号整数列表开始。
在 Arrow 中,这将被表示为 PrimitiveArray,它将它们连续存储在内存中
┌─────┐
│ 1 │
├─────┤
│ 2 │
├─────┤
│ 3 │
├─────┤
│ 4 │
└─────┘
Values
Parquet 有多种不同的编码可用于整数类型,其确切细节超出了本文的范围。广义上讲,数据将存储在一个或多个数据页中,其中包含编码形式的整数
┌─────┐
│ 1 │
├─────┤
| 2 │
├─────┤
│ 3 │
├─────┤
│ 4 │
└─────┘
Values
可空原始列
现在让我们考虑一个可空列的情况,其中一些值可能具有特殊的 Sentinel 值 NULL,表示“此值未知”。
在 Arrow 中,null 值与值分开存储,采用有效性位掩码的形式,相应位置的值缓冲区中包含任意数据。这种空间高效的编码意味着以下示例的整个有效性掩码使用 5 位存储
┌─────┐ ┌─────┐
│ 1 │ │ 1 │
├─────┤ ├─────┤
│ 0 │ │ ?? │
├─────┤ ├─────┤
│ 1 │ │ 3 │
├─────┤ ├─────┤
│ 1 │ │ 4 │
├─────┤ ├─────┤
│ 0 │ │ ?? │
└─────┘ └─────┘
Validity Values
在 Parquet 中,有效性信息也与值分开存储,但是,它不是编码为有效性位掩码,而是编码为 16 位整数列表,称为*定义级别*。与 Parquet 中的其他数据一样,这些整数定义级别使用高效编码存储,将在下一篇文章中详细阐述,但目前,定义级别为 1 表示有效值,0 表示空值。与 Arrow 不同,空值不会编码在值列表中
┌─────┐ ┌─────┐
│ 1 │ │ 1 │
├─────┤ ├─────┤
│ 0 │ │ 3 │
├─────┤ ├─────┤
│ 1 │ │ 4 │
├─────┤ └─────┘
│ 1 │
├─────┤
│ 0 │
└─────┘
Definition Values
Levels
接下来:嵌套和分层数据
掌握了 Arrow 和 Parquet 如何不同地存储可空性/定义的开创性理解后,我们准备转向更复杂的嵌套类型,您可以在我们关于该主题的下一篇博客文章中阅读相关内容。