Arrow 和 Parquet 第 1 部分:基本类型和可空性


已发布 2022 年 10 月 05 日
作者 tustvold 和 alamb

简介

我们最近在 Rust Apache Arrow 中完成了一个长期项目,以完成对读取和写入任意嵌套的 Parquet 和 Arrow 模式的支持。 这是一个复杂的话题,我们遇到了缺乏平易近人的技术信息的情况,因此撰写此博客与社区分享我们的学习成果。

Apache Arrow 是一种开放的、与语言无关的列式内存格式,用于扁平化和分层数据,旨在实现高效的分析操作。 Apache Parquet 是一种开放的、面向列的数据文件格式,旨在实现非常高效的数据编码和检索。

分析系统越来越普遍地使用 Arrow 来处理存储在 Parquet 文件中的数据,因此它们之间快速、高效和正确的转换是一个关键的构建块。

从历史上看,分析处理主要集中于查询具有表格模式的数据,其中具有固定数量的列,并且每一行都包含每一列的单个值。 然而,随着结构化文档格式(如 XML、JSON 等)的日益普及,仅支持表格模式可能会让用户感到沮丧,因为它需要通常非平凡的数据转换来首先扁平化文档数据。

截至 20.0.0 版本(于 2022 年 8 月发布),Rust Arrow 实现对读取结构化类型的支持已完成。 入门说明可以在这里找到,欢迎在我们的bug跟踪器上提出任何问题。

在本系列中,我们将解释 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 vs 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 有多种 不同的编码方式 可用于整数类型,其确切细节超出了本文的范围。 广义上讲,数据将存储在一个或多个 DataPage 中,其中包含编码形式的整数

┌─────┐
│  1  │
├─────┤
|  2  │
├─────┤
│  3  │
├─────┤
│  4  │
└─────┘
Values

可为空的原始列

现在让我们考虑一个可为空列的情况,其中一些值可能具有特殊的哨兵值 NULL,表示“此值未知”。

在 Arrow 中,null 与值分开存储,采用 有效性位图 的形式,值缓冲区中相应位置的任意数据。 这种节省空间的编码意味着以下示例的整个有效性掩码使用 5 位存储

┌─────┐   ┌─────┐
│  1  │   │  1  │
├─────┤   ├─────┤
│  0  │   │ ??  │
├─────┤   ├─────┤
│  1  │   │  3  │
├─────┤   ├─────┤
│  1  │   │  4  │
├─────┤   ├─────┤
│  0  │   │ ??  │
└─────┘   └─────┘
Validity   Values

在 Parquet 中,有效性信息也与值分开存储,但是,它不是编码为有效性位图,而是编码为称为定义级别的 16 位整数列表。 像 Parquet 中的其他数据一样,这些整数定义级别使用高效率编码存储,并将在下一篇文章中展开,但目前定义级别 1 表示有效值,0 表示 null 值。 与 Arrow 不同,null 不在值列表中编码

┌─────┐    ┌─────┐
│  1  │    │  1  │
├─────┤    ├─────┤
│  0  │    │  3  │
├─────┤    ├─────┤
│  1  │    │  4  │
├─────┤    └─────┘
│  1  │
├─────┤
│  0  │
└─────┘
Definition  Values
 Levels

接下来:嵌套和分层数据

掌握了 Arrow 和 Parquet 如何以不同方式存储可空性/定义的底层理解后,我们就可以继续讨论更复杂的嵌套类型,您可以在我们关于该主题的 下一篇博客文章中阅读相关内容