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 更好地利用并行机会。 SIMDILP 的细节远远超出了本文的范围,但重要的结论是,处理大块数据而无需干预条件分支具有巨大的性能优势。

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 如何不同地存储可空性/定义的基础知识后,我们就可以继续学习更复杂的嵌套类型,您可以在我们关于该主题的下一篇文章中阅读相关内容。