Arrow 与 Parquet 第一部分:基本类型和可空性
已发布 2022 年 10 月 5 日
作者 tustvold 和 alamb
引言
我们最近在 Rust Apache Arrow 中完成了一个长期运行的项目,以完成对读取和写入任意嵌套 Parquet 和 Arrow 模式的支持。这是一个复杂的话题,我们发现缺乏易于理解的技术信息,因此撰写此博客与社区分享我们的学习成果。
Apache Arrow 是一种开放的、与语言无关的列式内存格式,用于扁平化和分层数据,旨在实现高效的分析操作。Apache Parquet 是一种开放的、面向列的数据文件格式,旨在实现高效的数据编码和检索。
分析系统使用 Arrow 处理存储在 Parquet 文件中的数据越来越普遍,因此它们之间快速、高效且正确的转换是一个关键的构建块。
过去,分析处理主要集中于查询具有表格模式的数据,其中列数固定,每行包含每列的单个值。然而,随着 XML、JSON 等结构化文档格式的日益普及,仅支持表格模式可能会让用户感到沮丧,因为它需要进行通常非常重要的数据转换才能首先展平文档数据。
从 2022 年 8 月发布的 20.0.0 版本开始,用于读取结构化类型的 Rust Arrow 实现功能完整。入门说明可以在此处找到,请随时在我们的问题跟踪器上提出任何问题。
在本系列中,我们将解释 Parquet 和 Arrow 如何表示嵌套数据,重点介绍它们之间的异同,并简要介绍在格式之间转换的实用方法。
列式与面向记录
首先,有必要退一步讨论列式和面向记录的数据格式之间的区别。在面向记录的数据格式(例如换行分隔的 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 与 Arrow
Parquet 和 Arrow 是互补的技术,它们做出了一些不同的设计权衡。特别是,Parquet 是一种存储格式,旨在最大限度地提高空间效率,而 Arrow 是一种内存格式,旨在供矢量化计算内核操作。
主要区别在于 Arrow 提供对任何数组索引的 O(1)
随机访问查找,而 Parquet 不提供。特别是,Parquet 使用 dremel 记录粉碎、可变长度编码方案 和 块压缩 来大幅减少数据大小,但这些技术是以损失高性能随机访问查找为代价的。
一种发挥每种技术优势的常见模式是,以 Arrow 格式将来自压缩表示(例如 Parquet)的数据以千行批次的形式流式传输,分别处理这些批次,并将结果累积到更压缩的表示中。这受益于能够有效地对 Arrow 数据执行计算,同时控制内存需求,并允许计算内核与源和目标的编码无关。
Arrow 主要是一种内存格式,而 Parquet 是一种存储格式。
非可空基本列
让我们从最简单的非可空 32 位有符号整数列表开始。
在 Arrow 中,这将表示为 PrimitiveArray
,它将它们连续存储在内存中
┌─────┐
│ 1 │
├─────┤
│ 2 │
├─────┤
│ 3 │
├─────┤
│ 4 │
└─────┘
Values
Parquet 具有多种可用于整数类型的 不同编码,其确切细节超出了本文的范围。广义地说,数据将存储在一个或多个包含编码形式整数的 *数据页* 中
┌─────┐
│ 1 │
├─────┤
| 2 │
├─────┤
│ 3 │
├─────┤
│ 4 │
└─────┘
Values
可空基本列
现在让我们考虑可空列的情况,其中某些值可能具有特殊的标记值 NULL
,该值表示“此值未知”。
在 Arrow 中,空值以 有效性位掩码 的形式与值单独存储,值缓冲区中相应位置的数据是任意的。这种节省空间的编码意味着以下示例的整个有效性掩码使用 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 如何不同地存储可空性/定义的基础知识后,我们就可以继续学习更复杂的嵌套类型,您可以在我们关于该主题的下一篇文章中阅读相关内容。