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 更好地利用并行性。 SIMDILP 的具体细节远远超出了本文的范围,但重要的启示是,处理大量数据块而没有干预条件分支具有显著的性能优势。

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 如何不同地存储可空性/定义的开创性理解后,我们准备转向更复杂的嵌套类型,您可以在我们关于该主题的下一篇博客文章中阅读相关内容。