引言#

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 术语中,每一列称为一个**数组**。数组可以具有不同的数据类型,并且它们的值在内存中的存储方式在数据类型之间有所不同。这些值如何在内存中排列的规范就是我们所说的**物理内存布局**。存储数组数据的连续内存区域称为**缓冲区**。一个数组包含一个或多个缓冲区。

接下来的部分将介绍 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 还具有 Null 数据类型的概念,其中所有值都为空。在这种情况下,不会分配任何缓冲区。

可变长度二进制和字符串#

与固定大小的基本布局相反,可变长度布局允许表示一个数组,其中每个元素可以具有可变的字节大小。此布局用于二进制和字符串数据。

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

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

二进制/字符串与大二进制/字符串之间的区别在于偏移量数据类型。在第一种情况下是 int32,在第二种情况下是 int64。

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

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 是一个嵌套数据类型(父类型),它有一个子类型(列表中值的类型)。

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.

可变大小列表数据类型的物理布局图。#

固定大小列表#

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

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

固定大小列表数据类型的物理布局图。#

列表视图#

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

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.

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

结构体#

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

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

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

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

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

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

映射#

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

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

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

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

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

联合#

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

Arrow 定义了两种不同的联合数据类型,“密集”和“稀疏”。

密集联合#

密集联合为混合类型数组中存在的每个数据类型都提供一个子数组,以及两个自己的缓冲区

  • **类型缓冲区:**为数组的每个槽保存数据类型 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.

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

稀疏联合#

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

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.

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

运行结束编码布局#

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

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

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

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

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 术语概述#

物理布局 指定如何在内存中表示数组的值。

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

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

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

注意

分块数组是某些实现(如 Arrow C++ 和 PyArrow)特有的概念。

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

模式 字段的有序集合,用于传达像记录批次或表这样的对象的所有数据类型。模式可以包含可选的键/值元数据。

字段包含字段名称、数据类型、可空标志以及记录批次中特定列的可选键值元数据。

一个不连续的、二维的数据块,由分块数组的有序集合组成。所有分块数组都具有相同的长度,但可能具有不同的类型。不同的列可以以不同的方式分块。

注意

表是某些实现(如 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:extension:name''ARROW:extension:metadata' 键)来注释任何内置 Arrow 数据类型(“存储类型”)来定义扩展类型。

另请参阅

扩展类型 文档。

规范扩展类型#

共享众所周知的扩展类型的定义有利于提高集成 Arrow 列式数据的不同系统之间的互操作性。出于这个原因,规范扩展类型在 Arrow 本身中定义。

另请参阅

规范扩展类型 文档。

社区扩展类型#

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

示例

  • GeoArrow:用于表示矢量几何的 Arrow 扩展类型的集合

共享 Arrow 数据#

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

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

  • 要在同一进程中共享 Arrow 数据,可以使用Arrow C 数据接口,它用于在同一进程中的不同库之间在内存中零复制共享相同的缓冲区。