介绍#

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 列式格式 中找到。

对空值(Null Values)的支持#

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

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

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

注意

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

这也是我们在本文档包含的图表中表示有效性位图的方式。

原始布局(Primitive Layouts)#

定长原始布局(Fixed Size Primitive Layout)#

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

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

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

注意

布尔数据类型采用一种原始布局,其值以位(bit)而非字节(byte)编码。这意味着物理布局包含一个值位图缓冲区,以及可选的一个有效性位图缓冲区。

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

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

注意

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

变长二进制与字符串#

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

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

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

二进制/字符串与大二进制(Large Binary)/大字符串(Large String)的区别在于偏移量的数据类型。前者使用 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.

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

变长二进制与字符串视图(View)#

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

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

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

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.

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

嵌套布局(Nested Layouts)#

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

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

列表(List)#

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

列表数据类型的偏移量为 int32,而大列表(Large List)的偏移量为 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)#

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

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)中,而特定字段(键)的值保存在子数组中。

由于子数组是独立的,Arrow 不强制执行结构体有效性位图与其子级位图之间的物理一致性。在逻辑上,仅当父位图和子位图在该槽位的值均为 1 时(逻辑与运算),结构体行才有效。这允许在子数组的空结构体位置存在“隐藏”数据(请参阅下文的 alice)。

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)#

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

  • 类型缓冲区(Types buffer):为数组的每个槽位保存数据类型 ID。数据类型 ID 通常是子数组的索引;然而,数据类型 ID 与子数组索引之间的关系是数据类型的一个参数。

  • 偏移量缓冲区(Offsets buffer):为每个数组槽位保存指向相应子数组的相对偏移量。

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.

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

字典编码布局(Dictionary Encoded Layout)#

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

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

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

游程编码布局(Run-End Encoded Layout)#

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

  • 游程结束数组(Run ends array):保存数组中每个游程结束的索引。游程结束的数量与父数组的长度相同。

  • 值数组(Values array):实际的不重复值(连同空值)。

注意,父数组的空值会严格地反映在值数组中。

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

物理布局(Physical layout) 一种关于如何在内存中表示数组值的规范。

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

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

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

注意

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

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

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

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

表(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固定形状张量(Fixed shape tensor) 扩展类型。

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

另请参阅

扩展类型 文档。

规范扩展类型(Canonical Extension Types)#

共享众所周知的扩展类型定义,以提高集成 Arrow 列式数据的不同系统之间的互操作性是有益的。因此,规范扩展类型直接定义在 Arrow 中。

另请参阅

规范扩展类型 文档。

社区扩展类型(Community Extension Types)#

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

示例

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

共享 Arrow 数据#

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

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

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