介绍#

Apache Arrow 的诞生源于对一套围绕表格数据表示和系统间交换标准的需求。采用这些标准可以降低数据序列化/反序列化的计算成本,以及跨不同编程语言实现的系统的实现成本。

Apache Arrow 规范可以用任何编程语言实现,但也提供了许多语言的官方实现。一个实现由使用该语言提供的构造定义的格式定义以及常见的内存数据处理算法(例如切片和连接)组成。用户可以在自己选择的编程语言中扩展和使用 Apache Arrow 实现提供的实用工具。一些实现更为先进,并提供了大量用于内存中分析数据处理的算法。有关实现之间差异的更多详细信息,请参阅实现状态页面。

除了最初的愿景之外,Arrow 还发展成为一个多语言库集合,用于解决与内存中分析数据处理相关的问题。这涵盖了以下主题:

  • 零拷贝共享内存和基于 RPC 的数据移动

  • 读写文件格式(如 CSV、Apache ORCApache Parquet

  • 内存中分析和查询处理

Arrow 列式格式#

Apache Arrow 专注于表格数据。举个例子,假设我们有可以组织成表格的数据:

Diagram with tabular data of 4 rows and columns.

表格数据结构示意图。#

表格数据可以在内存中用基于行的格式或基于列的格式表示。行式格式逐行存储数据,这意味着行在计算机内存中是相邻的:

Tabular data being structured row by row in computer memory.

表格数据在内存中逐行保存。#

在列式格式中,数据是逐列组织的。这种组织方式由于内存局部性,使得过滤、分组、聚合等分析操作更加高效。在处理数据时,CPU 访问的内存位置往往彼此靠近。通过将数据在内存中保持连续,还可以实现计算的向量化。大多数现代 CPU 都有 SIMD 指令(单条指令操作多个值),从而能够使用单个 CPU 指令对向量数据进行并行处理和执行操作。

Apache Arrow 正是在解决这个问题。它是一个使用列式布局的规范。

Tabular data being structured column by column in computer memory.

相同的表格数据在内存中逐列保存。#

在 Arrow 的术语中,每一列称为一个 Array(数组)。数组可以有不同的数据类型,其值在内存中的存储方式因数据类型而异。我们把这些值在内存中如何排列的规范称为物理内存布局。一个存储数组数据的连续内存区域称为缓冲区 (Buffer)。一个数组由一个或多个缓冲区组成。

接下来的部分将介绍 Arrow 列式格式,解释不同的物理布局。完整的格式规范可以在Arrow 列式格式中找到。

对空值的支持#

Arrow 支持所有数据类型的缺失值或“空值”:数组中的任何值,无论是原始数据类型还是嵌套数据类型,在语义上都可以为空。

在 Arrow 中,一个专用的缓冲区,称为有效性(或“空值”)位图,与数据一起使用,指示数组中的每个值是否为空:值为 1 表示该值非空(“有效”),而值为 0 表示该值为空。

这个有效性位图是可选的:如果数组中没有缺失值,则不需要分配该缓冲区(如下图中的第 1 列示例)。

注意

由于使用了最低有效位编号,我们在一个 8 位组内从右到左读取有效性位图。

本文档中的图表也是这样表示有效性位图的。

原始布局#

定长原始布局#

原始列表示一个值数组,其中每个值都具有相同的物理大小(以字节为单位)。使用定长原始布局的数据类型有,例如,有符号和无符号整数数据类型、浮点数、布尔值、十进制数和时间数据类型。

Diagram is showing the difference between the primitive data type presented in a Table and the data actually stored in computer memory.

原始数据类型的物理布局图。#

注意

布尔数据类型用原始布局表示,其中值以位而不是字节进行编码。这意味着物理布局包括一个值位图缓冲区,可能还有一个有效性位图缓冲区。

Diagram is showing the difference between the boolean data type presented in a Table and the data actually stored in computer memory.

布尔数据类型的物理布局图。#

注意

Arrow 还有一个空数据类型的概念,其中所有值都为空。在这种情况下,不分配任何缓冲区。

变长二进制和字符串#

与定长原始布局相比,变长布局允许表示一个数组,其中每个元素的字节大小可以不同。此布局用于二进制和字符串数据。

二进制或字符串列中所有元素的字节都连续存储在单个缓冲区或内存区域中。为了知道列中每个元素的起止位置,物理布局还包括一个整数偏移量。偏移量缓冲区总是比数组多一个元素。最后两个偏移量定义了最后一个二进制/字符串元素的开始和结束。

二进制和字符串数据类型共享相同的物理布局。它们唯一的区别是,字符串类型的数组被假定为包含有效的 UTF-8 字符串数据。

binary/string 和 large binary/large string 的区别在于偏移量的数据类型。前者是 int32,后者是 int64。

使用 32 位偏移量的数据类型的限制是,每个数组的最大大小为 2GB。对于更大的数据,仍然可以使用非 large 变体,但需要多个块。

Diagram is showing the difference between the variable length string data type presented in a Table and the data actually stored in computer memory.

变长字符串数据类型的物理布局图。#

变长二进制和字符串视图#

此布局是变长二进制布局的一种替代方案,改编自慕尼黑工业大学的 UmbraDB,并且类似于 DuckDBVelox 中使用的字符串布局(有时也称为“德国字符串”)。

与传统的二进制和字符串布局的主要区别在于视图缓冲区。它包含字符串的长度,然后要么是其内联出现的字符(对于小字符串),要么是字符串的前 4 个字节和一个指向可能是多个数据缓冲区之一的偏移量。因为它使用偏移量和长度来引用数据缓冲区,所以所有元素的字节不需要连续存储在单个缓冲区中。这使得可以将变长元素乱序写入数组。

这些特性对于高效的字符串处理非常重要。前缀为字符串比较提供了一个有利的快速路径,因为比较结果通常在前四个字节内就能确定。选择元素是对定宽视图缓冲区的简单“收集”操作,不需要重写值缓冲区。

Diagram is showing the difference between the variable length string view data type presented in a Table and the data actually stored in computer memory.

变长字符串视图数据类型的物理布局图。#

嵌套布局#

嵌套数据类型引入了父数组和子数组的概念。它们表达了嵌套数据类型结构中物理值数组之间的关系。

嵌套数据类型依赖于一个或多个其他子数据类型。例如,列表是一种嵌套数据类型(父类型),它有一个子类型(列表中值的数据类型)。

列表 (List)#

列表数据类型能够表示一个数组,其中每个元素都是相同数据类型元素的序列。该布局类似于变长二进制或字符串布局,因为它有一个偏移量缓冲区来定义每个元素的值序列的开始和结束位置,所有值都连续存储在值子数组中。

列表数据类型中的偏移量是 int32,而在大列表中,偏移量是 int64。

Diagram is showing the difference between the variable size list data type presented in a Table and the data actually stored in computer memory.

变长列表数据类型的物理布局图。#

定长列表 (Fixed Size List)#

定长列表是变长列表的一种特殊情况,其中每个列槽包含一个固定大小的序列,这意味着所有列表的大小都相同,因此不再需要偏移量缓冲区。

Diagram is showing the difference between the fixed size list data type presented in a Table and the data actually stored in computer memory.

定长列表数据类型的物理布局图。#

列表视图 (List View)#

与列表类型相比,列表视图类型除了偏移量缓冲区外,还有一个大小缓冲区。偏移量继续指示每个元素的开始位置,但大小现在保存在一个单独的大小缓冲区中。这允许乱序的偏移量,因为大小不再从连续的偏移量中推导出来。

Diagram is showing the difference between the variable size list view data type presented in a Table and the data actually stored in computer memory.

变长列表视图数据类型的物理布局图。#

结构体 (Struct)#

结构体是一种嵌套数据类型,由一个有序的字段序列(数据类型和名称)参数化。

  • 每个字段都有一个子数组。

  • 子数组是独立的,不必在内存中彼此相邻。它们只需要具有相同的长度。

可以把单个结构体字段看作是一个键值对,其中键是字段名,子数组是其值。字段(键)保存在模式 (schema) 中,特定字段(键)的值保存在子数组中。

Diagram is showing the difference between the struct data type presented in a Table and the data actually stored in computer memory.

结构体数据类型的物理布局图。#

映射 (Map)#

映射数据类型表示嵌套数据,其中每个值都是可变数量的键值对。其物理表示与 {key, value} 结构体的列表相同。

结构体和映射数据类型之间的区别在于,结构体将键保存在模式中,要求键为字符串,并且值存储在子数组中,每个字段一个。可以有多个键,因此有多个子数组。另一方面,映射有一个子数组保存所有不同的键(因此所有键都需要是相同的数据类型,但不一定是字符串),第二个子数组保存所有值。值需要是相同的数据类型;然而,该数据类型不必与键的数据类型匹配。

此外,映射将结构体存储在列表中,并且由于列表是可变形状的,因此需要一个偏移量。

Diagram is showing the difference between the map data type presented in a Table and the data actually stored in computer memory.

映射数据类型的物理布局图。#

联合体 (Union)#

联合体是一种嵌套数据类型,其中联合体中的每个槽都有一个值,其数据类型从可能的 Arrow 数据类型的子集中选择。这意味着联合体数组表示一个混合类型的数组。与其他数据类型不同,联合体没有自己的有效性位图,其空值性由子数组决定。

Arrow 定义了两种不同的联合体数据类型,“稠密 (dense)”和“稀疏 (sparse)”。

稠密联合体 (Dense Union)#

稠密联合体为混合类型数组中存在的每种数据类型都有一个子数组,并有自己的两个缓冲区:

  • 类型缓冲区:保存数组每个槽的数据类型 ID。数据类型 ID 通常是子数组的索引;但是,数据类型 ID 和子索引之间的关系是数据类型的一个参数。

  • 偏移量缓冲区:保存每个数组槽在相应子数组中的相对偏移量。

Diagram is showing the difference between the dense union data type presented in a Table and the data actually stored in computer memory.

稠密联合体数据类型的物理布局图。#

稀疏联合体 (Sparse union)#

稀疏联合体与稠密联合体结构相同,但省略了偏移量缓冲区。在这种情况下,每个子数组的长度都等于联合体的长度。

Diagram is showing the difference between the sparse union data type presented in a Table and the data actually stored in computer memory.

稀疏联合体数据类型的物理布局图。#

字典编码布局#

当数据中包含许多重复值时,字典编码会很有效。值由引用字典的整数表示,字典通常由唯一值组成。

Diagram is showing the difference between the dictionary data type presented in a Table and the data actually stored in computer memory.

字典数据类型的物理布局图。#

行程编码布局#

行程编码非常适合表示包含相同值序列的数据。这些序列称为行程 (run)。行程编码数组本身没有缓冲区,但有两个子数组:

  • 行程结束数组:保存每个行程在数组中结束的索引。行程结束的数量与其父数组的长度相同。

  • 值数组:不重复的实际值(以及空值)。

请注意,父数组的空值严格表示在值数组中。

Diagram is showing the difference between the run-end encoded data type presented in a Table and the data actually stored in computer memory.

行程编码数据类型的物理布局图。#

另请参阅

所有 Arrow 数据类型的表格。

Arrow 术语概述#

物理布局 一个用于表示数组值在内存中如何存储的规范。

缓冲区 (Buffer) 一个具有给定字节长度的连续内存区域。缓冲区用于存储数组的数据。有时我们使用缓冲区中元素数量的概念,但这只有在我们知道包装此特定缓冲区的数组的数据类型时才能使用。

数组 (Array) 一个连续的、一维的、长度已知的值序列,其中所有值都具有相同的数据类型。一个数组由零个或多个缓冲区组成。

分块数组 (Chunked Array) 一个非连续的、一维的、长度已知的值序列,其中所有值都具有相同的数据类型。由零个或多个数组(即“块”)组成。

注意

分块数组是特定于某些实现的概念,例如 Arrow C++ 和 PyArrow。

记录批次 (RecordBatch) 一个连续的、二维的数据结构,由一组有序的、长度相同的数组组成。

模式 (Schema) 一个有序的字段集合,用于传达诸如记录批次或表之类的对象的所有数据类型。模式可以包含可选的键/值元数据。

字段 (Field) 一个字段包括记录批次中特定列的字段名、数据类型、可空性标志和可选的键值元数据。

表 (Table) 一个非连续的、二维的数据块,由一组有序的分块数组组成。所有分块数组的长度相同,但可以有不同的类型。不同的列可以有不同的分块方式。

注意

表是特定于某些实现的概念,例如 Arrow C++ 和 PyArrow。例如,在 Java 实现中,表不是分块数组的集合,而是记录批次的集合。

A graphical representation of an Arrow Table and a Record Batch, with structure as described in text above.

另请参阅

更多术语请见术语表

扩展类型#

如果系统或应用程序需要用自定义语义来扩展标准的 Arrow 数据类型,可以通过定义扩展类型来实现。

扩展类型的例子有 UUID定形张量 扩展类型。

扩展类型可以通过注解任何内置的 Arrow 数据类型(“存储类型”),并附上自定义类型名称和可选的序列化表示(字段元数据结构中的 'ARROW:extension:name''ARROW:extension:metadata' 键)来定义。

另请参阅

扩展类型文档。

规范扩展类型#

共享知名扩展类型的定义是有益的,可以改善集成 Arrow 列式数据的不同系统之间的互操作性。因此,Arrow 本身定义了规范扩展类型。

另请参阅

规范扩展类型文档。

社区扩展类型#

这些是在特定领域内已确立为标准的 Arrow 扩展类型。

示例

  • GeoArrow:一组用于表示矢量几何的 Arrow 扩展类型。

共享 Arrow 数据#

Arrow 内存布局旨在成为在内存中表示表格数据的通用标准,不与特定实现绑定。Arrow 标准定义了两种协议,用于在应用程序之间进行明确且无歧义的 Arrow 数据通信:

  • 用于在进程之间或通过网络共享 Arrow 数据的协议称为序列化和进程间通信 (IPC)。共享数据的规范称为 IPC 消息格式,它定义了如何将 Arrow 数组或记录批次缓冲区堆叠在一起以进行序列化和反序列化。

  • 为了在同一进程中共享 Arrow 数据,使用Arrow C 数据接口,旨在在同一进程内的不同库之间以零拷贝方式共享相同的内存缓冲区。