Arrow 列式格式#

版本: 1.5

Arrow 列式格式 包括一个与语言无关的内存中数据结构规范、元数据序列化以及用于序列化和通用数据传输的协议。

本文档旨在提供足够的细节,以便在没有现有实现帮助的情况下创建列式格式的新实现。我们利用 Google 的 Flatbuffers 项目进行元数据序列化,因此在阅读本文档时,需要参考该项目的 Flatbuffers 协议定义文件

列式格式具有一些关键特性

  • 数据相邻性,用于顺序访问(扫描)

  • O(1)(常数时间)随机访问 [1]

  • 对 SIMD 和向量化友好

  • 可重定位,无需“指针混淆”,允许在共享内存中实现真正的零拷贝访问

Arrow 列式格式以相对更昂变的写入操作为代价,提供了分析性能和数据局部性保证。本文档仅关注内存中的数据表示和序列化细节;有关协调数据结构写入等问题,则留待实现方处理。

术语#

由于不同项目对各种概念使用了不同的词语,这里提供一个小词汇表以帮助消歧义。

  • 数组 (Array) 或 向量 (Vector):具有已知长度且所有值类型相同的序列。这两个术语在不同的 Arrow 实现中可以互换使用,但在本文档中我们使用“数组”。

  • (Slot):在具有特定数据类型的数组中的单个逻辑值

  • 缓冲区 (Buffer) 或 连续内存区域 (Contiguous memory region):具有给定长度的顺序虚拟地址空间。任何字节都可以通过小于该区域长度的单个指针偏移量访问。

  • 物理布局 (Physical Layout):不考虑任何值语义的数组的底层内存布局。例如,32 位有符号整数数组和 32 位浮点数数组具有相同的布局。

  • 数据类型 (Data type):一个面向应用的语义值类型,通过某种物理布局实现。例如,Decimal128 值以 16 字节存储在固定大小的二进制布局中。时间戳可以存储为 64 位固定大小的布局。

  • 基本类型 (Primitive type):没有子类型的数值类型。这包括固定位宽、可变大小二进制和 Null 类型等类型。

  • 嵌套类型 (Nested type):其完整结构依赖于一个或多个其他子类型的数值类型。两个完全指定(fully-specified)的嵌套类型相等当且仅当它们的子类型相等。例如,List<U>List<V> 不同,当且仅当 U 和 V 是不同的类型时。

  • 父数组 (Parent) 和 子数组 (child arrays):用于表达嵌套类型结构中物理值数组之间关系的名称。例如,List<T> 类型的父数组有一个 T 类型的子数组(下面将详细介绍列表)。

  • 参数化类型 (Parametric type):需要附加参数才能完全确定其语义的类型。例如,所有嵌套类型在结构上都是参数化的。时间戳也是参数化的,因为它需要一个单位(如微秒)和一个时区。

数据类型#

文件 Schema.fbs 定义了 Arrow 列式格式支持的内置数据类型。每种数据类型都使用明确定义的物理布局。

Schema.fbs 是标准 Arrow 数据类型描述的权威来源。但是,我们也提供以下表格以供参考

类型

类型参数 (1)

物理内存布局

Null

Null

Boolean

固定大小基本类型

整数

  • 位宽

  • 有符号性

“ (与上面相同)

浮点数

  • 精度

Decimal

  • 位宽

  • 精度

  • 精度

日期型 (Date)

  • 单位

时间

  • 位宽 (2)

  • 单位

Timestamp

  • 单位

  • 时区

间隔

  • 单位

持续时间

  • 单位

固定大小二进制

  • 字节宽度

固定大小二进制

Binary

带有 32 位偏移量的可变大小二进制

Utf8 字符串

大型二进制

带有 64 位偏移量的可变大小二进制

大型 Utf8

二进制视图

可变大小二进制视图

Utf8 视图

固定大小列表

  • 值类型

  • 列表大小

固定大小列表

列表

  • 值类型

带有 32 位偏移量的可变大小列表

大型列表

  • 值类型

带有 64 位偏移量的可变大小列表

列表视图

  • 值类型

带有 32 位偏移量和大小的可变大小列表视图

大型列表视图

  • 值类型

带有 64 位偏移量和大小的可变大小列表视图

结构体

  • 子元素

结构体

Map

  • 子元素

  • 键的排序性

结构体的可变大小列表

联合

  • 子元素

  • 模式

  • 类型 ID

稠密或稀疏联合 (3)

字典

  • 索引类型 (4)

  • 值类型

  • 顺序性

字典编码

运行末尾编码

  • 运行末尾类型 (5)

  • 值类型

运行末尾编码

  • (1) 斜体显示的类型参数表示数据类型的子类型。

  • (2) Time 类型的位宽参数在技术上是多余的,因为每个单位都规定了单个位宽。

  • (3) Union 类型是使用稀疏布局还是稠密布局由其模式参数决定。

  • (4) Dictionary 类型的索引类型只能是整数类型,最好是有符号的,宽度在 8 到 64 位之间。

  • (5) Run-End Encoded 类型的运行末尾类型只能是宽度在 16 到 64 位的有符号整数类型。

注意

有时术语“逻辑类型”用于指代 Arrow 数据类型,并将其与它们各自的物理布局区分开来。然而,与 Apache Parquet 等其他类型系统不同,Arrow 类型系统没有物理类型和逻辑类型的独立概念。

Arrow 类型系统单独提供了 扩展类型,它允许使用更丰富的面向应用的语义来注释标准 Arrow 数据类型(例如,定义基于标准 String 数据类型的“JSON”类型)。

物理内存布局#

数组由一些元数据和数据定义

  • 一种数据类型。

  • 一系列缓冲区。

  • 一个长度,表示为 64 位有符号整数。允许实现将长度限制在 32 位有符号整数,详见下文。

  • 一个 Null 计数,表示为 64 位有符号整数。

  • 一个可选的字典,用于字典编码的数组。

嵌套数组此外还有一个或多个这些项的集合,称为子数组

每种数据类型都有明确定义的物理布局。以下是 Arrow 定义的不同物理布局

  • 基本类型(固定大小):一系列值,每个值具有相同的字节或位宽度

  • 可变大小二进制:一系列值,每个值具有可变的字节长度。此布局支持使用 32 位和 64 位长度编码的两种变体。

  • 可变大小二进制视图:一系列值,每个值具有可变的字节长度。与可变大小二进制不同,此布局的值分布在多个缓冲区中,而不是密集且顺序地打包在一个缓冲区中。

  • 固定大小列表:一种嵌套布局,其中每个值都包含来自子数据类型的固定数量的元素。

  • 可变大小列表:一种嵌套布局,其中每个值是来自子数据类型的可变长度序列。此布局支持使用 32 位和 64 位长度编码的两种变体。

  • 可变大小列表视图:一种嵌套布局,其中每个值是来自子数据类型的可变长度序列。此布局与可变大小列表的不同之处在于它有一个附加的缓冲区,包含每个列表值的大小。这消除了对偏移量缓冲区的限制——它不需要按顺序排列。

  • 结构体 (Struct):一种嵌套布局,由命名字段的集合组成,每个字段的长度相同,但类型可能不同。

  • 稀疏稠密联合 (Sparse and Dense Union):一种嵌套布局,表示一个值序列,每个值都可以从子数组类型集合中选择类型。

  • 字典编码 (Dictionary-Encoded):一种布局,由一系列整数(任意位宽)组成,这些整数代表字典中的索引,字典可以是任何类型。

  • 运行末尾编码 (REE) (Run-End Encoded):一种嵌套布局,由两个子数组组成,一个代表值,一个代表相应值运行结束的逻辑索引。

  • Null:一个全为 Null 值的序列。

Arrow 列式内存布局仅适用于数据而非元数据。实现可以自由地以任何方便的形式在内存中表示元数据。我们使用 Flatbuffers 以与实现无关的方式处理元数据序列化,如下文所述。

缓冲区对齐和填充#

建议实现将内存分配在对齐的地址上(8 或 64 字节的倍数),并填充(过度分配)到 8 或 64 字节的倍数长度。在为进程间通信序列化 Arrow 数据时,会强制执行这些对齐和填充要求。如果可能,我们建议优先使用 64 字节对齐和填充。除非另有说明,否则填充的字节不需要具有特定值。

对齐要求遵循优化内存访问的最佳实践

  • 数值数组中的元素将保证通过对齐访问检索。

  • 在某些架构上,对齐有助于限制部分使用的缓存行。

推荐 64 字节对齐的依据是 Intel 性能指南,该指南建议内存对齐以匹配 SIMD 寄存器宽度。选择特定的填充长度是因为它与广泛部署的 x86 架构(Intel AVX-512)上最大的 SIMD 指令寄存器相匹配。

推荐的 64 字节填充允许在循环中一致地使用 SIMD 指令而无需额外的条件检查。这应该能带来更简单、高效且对 CPU 缓存友好的代码。换句话说,我们可以将整个 64 字节缓冲区加载到 512 位宽的 SIMD 寄存器中,并对打包到 64 字节缓冲区中的所有列值实现数据级并行。保证的填充也可以允许某些编译器直接生成更优化的代码(例如,可以使用 Intel 的 -qopt-assume-safe-padding)。

数组长度#

数组长度在 Arrow 元数据中表示为 64 位有符号整数。即使实现仅支持最大 32 位有符号整数的长度,该 Arrow 实现仍被视为有效。如果在多语言环境中使用 Arrow,我们建议将长度限制在 2 31 - 1 个元素或更少。可以使用多个数组块来表示更大的数据集。

Null 计数#

Null 值槽的数量是物理数组的一个属性,被视为数据结构的一部分。Null 计数在 Arrow 元数据中表示为 64 位有符号整数,因为它可能与数组长度一样大。

有效性位图#

数组中的任何值都可以是逻辑上的 Null,无论是基本类型还是嵌套类型。

除联合类型(稍后讨论)外,所有数组类型都使用专用的内存缓冲区,称为有效性(或“Null”)位图,来编码每个值槽的 Null 或非 Null 性。有效性位图必须足够大,以便为每个数组槽至少有 1 位。

任何数组槽是否有效(非 Null)都编码在该位图的相应位中。索引 j 处的 1(设置位)表示该值非 Null,而 0(未设置位)表示该值为 Null。位图应在分配时初始化为全部未设置(这也包括填充)。

is_valid[j] -> bitmap[j / 8] & (1 << (j % 8))

我们使用 最低有效位 (LSB) 编号(也称为位序)。这意味着在 8 位组中,我们从右向左读取。

values = [0, 1, null, 2, null, 3]

bitmap
j mod 8   7  6  5  4  3  2  1  0
          0  0  1  0  1  0  1  1

Null 计数为 0 的数组可以选择不分配有效性位图;如何表示取决于实现(例如,C++ 实现可能会使用 NULL 指针来表示这种“缺失”的有效性位图)。实现也可以出于方便总是分配有效性位图。Arrow 数组的消费者应准备好处理这两种可能性。

嵌套类型数组(如上所述,联合类型除外)具有自己的顶级有效性位图和 Null 计数,而与其子数组的 Null 计数和有效位无关。

Null 数组槽不需要具有特定值;任何“被遮盖”的内存都可以具有任何值,并且不需要被清零,尽管实现通常选择将 Null 值对应的内存清零。

固定大小基本类型布局#

基本值数组代表一个值数组,其中每个值都具有相同的物理槽宽度,通常以字节为单位,尽管规范也为位打包类型(例如,以位编码的布尔值)提供了支持。

在内部,该数组包含一个连续的内存缓冲区,其总大小至少等于槽宽度乘以数组长度。对于位打包类型,大小向上取整到最接近的字节。

相关的有效性位图是连续分配的(如上所述),但不需要在内存中与值缓冲区相邻。

布局示例:Int32 数组

例如,一个 int32 数组

[1, null, 2, 4, 8]

看起来像

* Length: 5, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00011101                 | 0 (padding)           |

* Value Buffer:

  | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63           |
  |-------------|-------------|-------------|-------------|-------------|-----------------------|
  | 1           | unspecified | 2           | 4           | 8           | unspecified (padding) |

布局示例:非 Null int32 数组

[1, 2, 3, 4, 8] 有两种可能的布局

* Length: 5, Null count: 0
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00011111                 | 0 (padding)           |

* Value Buffer:

  | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63           |
  |-------------|-------------|-------------|-------------|-------------|-----------------------|
  | 1           | 2           | 3           | 4           | 8           | unspecified (padding) |

或者省略位图

* Length 5, Null count: 0
* Validity bitmap buffer: Not required
* Value Buffer:

  | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | bytes 12-15 | bytes 16-19 | Bytes 20-63           |
  |-------------|-------------|-------------|-------------|-------------|-----------------------|
  | 1           | 2           | 3           | 4           | 8           | unspecified (padding) |

可变大小二进制布局#

此布局中的每个值由 0 个或多个字节组成。虽然基本类型数组只有一个值缓冲区,但可变大小二进制具有一个偏移量缓冲区和一个数据缓冲区。

偏移量缓冲区包含 长度 + 1 个有符号整数(32 位或 64 位,取决于数据类型),用于对数据缓冲区中每个槽的起始位置进行编码。每个槽中值的长度是通过计算该槽索引处的偏移量与后续偏移量之间的差值来确定的。例如,槽 j 的位置和长度的计算方式如下:

slot_position = offsets[j]
slot_length = offsets[j + 1] - offsets[j]  // (for 0 <= j < length)

需要注意的是,Null 值可能具有正的槽长度。也就是说,Null 值可以在数据缓冲区中占用非空的内存空间。如果发生这种情况,相应内存空间的内容是未定义的。

偏移量必须单调递增,即 offsets[j+1] >= offsets[j],对于 0 <= j < 长度,即使对于 Null 槽也是如此。此属性确保所有值的位置都是有效且明确定义的。

通常,偏移量数组的第一个槽为 0,最后一个槽为值数组的长度。序列化此布局时,我们建议将偏移量标准化为从 0 开始。

布局示例:``VarBinary``

['joe', null, null, 'mark']

将表示如下

* Length: 4, Null count: 2
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001001                 | 0 (padding)           |

* Offsets buffer:

  | Bytes 0-19     | Bytes 20-63           |
  |----------------|-----------------------|
  | 0, 3, 3, 3, 7  | unspecified (padding) |

 * Value buffer:

  | Bytes 0-6      | Bytes 7-63            |
  |----------------|-----------------------|
  | joemark        | unspecified (padding) |

可变大小二进制视图布局#

注意

Arrow 列式格式 1.4 中的新特性

此布局中的每个值由 0 个或多个字节组成。这些字节的位置由一个视图缓冲区指示,该缓冲区可以指向可能存在的多个数据缓冲区之一,或者可以直接包含字符(内联)。

视图缓冲区包含 长度 个具有以下布局的视图结构

* Short strings, length <= 12
  | Bytes 0-3  | Bytes 4-15                            |
  |------------|---------------------------------------|
  | length     | data (padded with 0)                  |

* Long strings, length > 12
  | Bytes 0-3  | Bytes 4-7  | Bytes 8-11 | Bytes 12-15 |
  |------------|------------|------------|-------------|
  | length     | prefix     | buf. index | offset      |

在长字符串和短字符串情况下,前四个字节对字符串的长度进行编码,可用于确定应如何解释视图的其余部分。

在短字符串情况下,字符串的字节是内联的——存储在视图本身中,位于长度后面的 12 个字节内。字符串本身之后的任何剩余字节都用 0 填充。

在长字符串情况下,一个缓冲区索引指示哪个数据缓冲区存储数据字节,一个偏移量指示数据字节在该缓冲区中的起始位置。缓冲区索引 0 指向第一个数据缓冲区,即有效性缓冲区和视图缓冲区之后的第一个缓冲区。半开区间 [offset, offset + length) 必须完全包含在指定的缓冲区内。字符串前四个字节的副本内联存储在前缀中,位于长度之后。此前缀有助于实现有利的快速路径字符串比较,通常在头四个字节内确定。

所有整数(长度、缓冲区索引和偏移量)都是有符号的。

此布局改编自慕尼黑工业大学的 UmbraDB

请注意,此布局在 Arrow C 数据接口 中使用一个额外的缓冲区来存储可变长度的缓冲区长度。

可变大小列表布局#

列表是一种嵌套类型,在语义上类似于可变大小二进制。有两种列表布局变体——“list”和“list-view”——每种变体都可以由 32 位或 64 位的偏移量整数分隔。

列表布局#

列表布局由两个缓冲区、一个有效性位图和一个偏移量缓冲区,以及一个子数组定义。偏移量与可变大小二进制情况下的偏移量相同,支持 32 位和 64 位有符号整数偏移量作为偏移量的选项。这些偏移量不是引用另一个数据缓冲区,而是引用子数组。

与可变大小二进制布局类似,Null 值可能对应于子数组中一个非空片段。如果发生这种情况,相应片段的内容可以是任意的。

列表类型指定为 List<T>,其中 T 是任何类型(基本类型或嵌套类型)。在这些示例中,我们使用 32 位偏移量,而 64 位偏移量版本将表示为 LargeList<T>

布局示例:``List<Int8>`` 数组

我们以长度为 4 的 List<Int8> 示例来说明,其值为

[[12, -7, 25], null, [0, -127, 127, 50], []]

将具有以下表示

* Length: 4, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001101                 | 0 (padding)           |

* Offsets buffer (int32)

  | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63           |
  |------------|-------------|-------------|-------------|-------------|-----------------------|
  | 0          | 3           | 3           | 7           | 7           | unspecified (padding) |

* Values array (Int8Array):
  * Length: 7,  Null count: 0
  * Validity bitmap buffer: Not required
  * Values buffer (int8)

    | Bytes 0-6                    | Bytes 7-63            |
    |------------------------------|-----------------------|
    | 12, -7, 25, 0, -127, 127, 50 | unspecified (padding) |

布局示例:``List<List<Int8>>``

[[[1, 2], [3, 4]], [[5, 6, 7], null, [8]], [[9, 10]]]

将表示如下

* Length 3
* Nulls count: 0
* Validity bitmap buffer: Not required
* Offsets buffer (int32)

  | Bytes 0-3  | Bytes 4-7  | Bytes 8-11 | Bytes 12-15 | Bytes 16-63           |
  |------------|------------|------------|-------------|-----------------------|
  | 0          |  2         |  5         |  6          | unspecified (padding) |

* Values array (`List<Int8>`)
  * Length: 6, Null count: 1
  * Validity bitmap buffer:

    | Byte 0 (validity bitmap) | Bytes 1-63  |
    |--------------------------|-------------|
    | 00110111                 | 0 (padding) |

  * Offsets buffer (int32)

    | Bytes 0-27           | Bytes 28-63           |
    |----------------------|-----------------------|
    | 0, 2, 4, 7, 7, 8, 10 | unspecified (padding) |

  * Values array (Int8):
    * Length: 10, Null count: 0
    * Validity bitmap buffer: Not required

      | Bytes 0-9                     | Bytes 10-63           |
      |-------------------------------|-----------------------|
      | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 | unspecified (padding) |

列表视图布局#

注意

Arrow 列式格式 1.4 中的新特性

ListView 布局由三个缓冲区定义:一个有效性位图、一个偏移量缓冲区和一个附加的大小缓冲区。大小和偏移量具有相同的位宽,并且支持 32 位和 64 位有符号整数选项。

与 List 布局一样,偏移量对子数组的起始位置进行编码。与 List 布局不同,列表长度明确存储在大小缓冲区中而不是推断的。这使得偏移量可以无序。子数组的元素不必以其在父数组列表元素中逻辑出现的顺序存储。

每个列表视图值,包括 Null 值,都必须保证以下不变量

0 <= offsets[i] <= length of the child array
0 <= offsets[i] + size[i] <= length of the child array

列表视图类型指定为 ListView<T>,其中 T 是任何类型(基本类型或嵌套类型)。在这些示例中,我们使用 32 位偏移量和大小,而 64 位版本将表示为 LargeListView<T>

布局示例:``ListView<Int8>`` 数组

我们以长度为 4 的 ListView<Int8> 示例来说明,其值为

[[12, -7, 25], null, [0, -127, 127, 50], []]

它可以具有以下表示

* Length: 4, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001101                 | 0 (padding)           |

* Offsets buffer (int32)

  | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-63           |
  |------------|-------------|-------------|-------------|-----------------------|
  | 0          | 7           | 3           | 0           | unspecified (padding) |

* Sizes buffer (int32)

  | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-63           |
  |------------|-------------|-------------|-------------|-----------------------|
  | 3          | 0           | 4           | 0           | unspecified (padding) |

* Values array (Int8Array):
  * Length: 7,  Null count: 0
  * Validity bitmap buffer: Not required
  * Values buffer (int8)

    | Bytes 0-6                    | Bytes 7-63            |
    |------------------------------|-----------------------|
    | 12, -7, 25, 0, -127, 127, 50 | unspecified (padding) |

布局示例:``ListView<Int8>`` 数组

我们继续使用 ListView<Int8> 类型,但此实例说明了无序的偏移量和子数组值的共享。这是一个长度为 5 的数组,其逻辑值为

[[12, -7, 25], null, [0, -127, 127, 50], [], [50, 12]]

它可以具有以下表示

* Length: 5, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00011101                 | 0 (padding)           |

* Offsets buffer (int32)

  | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63           |
  |------------|-------------|-------------|-------------|-------------|-----------------------|
  | 4          | 7           | 0           | 0           | 3           | unspecified (padding) |

* Sizes buffer (int32)

  | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63           |
  |------------|-------------|-------------|-------------|-------------|-----------------------|
  | 3          | 0           | 4           | 0           | 2           | unspecified (padding) |

* Values array (Int8Array):
  * Length: 7,  Null count: 0
  * Validity bitmap buffer: Not required
  * Values buffer (int8)

    | Bytes 0-6                    | Bytes 7-63            |
    |------------------------------|-----------------------|
    | 0, -127, 127, 50, 12, -7, 25 | unspecified (padding) |

固定大小列表布局#

固定大小列表是一种嵌套类型,其中每个数组槽包含一个固定大小的序列,所有元素都具有相同的类型。

固定大小列表类型指定为 FixedSizeList<T>[N],其中 T 是任何类型(基本类型或嵌套类型),N 是表示列表长度的 32 位有符号整数。

固定大小列表数组由一个值数组表示,该值数组是类型 T 的子数组。T 也可能是嵌套类型。固定大小列表数组槽 j 中的值存储在值数组中一个从偏移量 j * N 开始的 N 长的切片中。

布局示例:``FixedSizeList<byte>[4]`` 数组

这里我们说明 FixedSizeList<byte>[4]

对于长度为 4 且相应值为的数组

[[192, 168, 0, 12], null, [192, 168, 0, 25], [192, 168, 0, 1]]

将具有以下表示

* Length: 4, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001101                 | 0 (padding)           |

* Values array (byte array):
  * Length: 16,  Null count: 0
  * validity bitmap buffer: Not required

    | Bytes 0-3       | Bytes 4-7   | Bytes 8-15                      |
    |-----------------|-------------|---------------------------------|
    | 192, 168, 0, 12 | unspecified | 192, 168, 0, 25, 192, 168, 0, 1 |

结构体布局#

结构体是一种嵌套类型,由类型的有序序列(所有类型都可以不同)参数化,称为其字段。每个字段必须有一个 UTF8 编码的名称,这些字段名称是类型元数据的一部分。

在物理上,结构体数组为每个字段有一个子数组。子数组是独立的,不一定在内存中相邻。结构体数组还有一个有效性位图,用于编码顶层有效性信息。

例如,结构体(为说明目的,字段名称显示为字符串)

Struct <
  name: VarBinary
  age: Int32
>

有两个子数组,一个是 VarBinary 数组(使用可变大小二进制布局),另一个是具有 Int32 逻辑类型的 4 字节基本值数组。

布局示例:``Struct<VarBinary, Int32>``

对于 [{'joe', 1}, {null, 2}, null, {'mark', 4}] 的布局,其子数组为 ['joe', null, 'alice', 'mark'][1, 2, null, 4] 将是

* Length: 4, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001011                 | 0 (padding)           |

* Children arrays:
  * field-0 array (`VarBinary`):
    * Length: 4, Null count: 1
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00001101                 | 0 (padding)           |

    * Offsets buffer:

      | Bytes 0-19     | Bytes 20-63           |
      |----------------|-----------------------|
      | 0, 3, 3, 8, 12 | unspecified (padding) |

     * Value buffer:

      | Bytes 0-11     | Bytes 12-63           |
      |----------------|-----------------------|
      | joealicemark   | unspecified (padding) |

  * field-1 array (int32 array):
    * Length: 4, Null count: 1
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00001011                 | 0 (padding)           |

    * Value Buffer:

      | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-63           |
      |-------------|-------------|-------------|-------------|-----------------------|
      | 1           | 2           | unspecified | 4           | unspecified (padding) |

结构体有效性#

结构体数组有其自己的有效性位图,独立于其子数组的有效性位图。结构体数组的有效性位图可能指示一个 Null,即使其一个或多个子数组在其相应槽中具有非 Null 值;反之亦然,子数组可能在其有效性位图中指示一个 Null,而结构体数组的有效性位图显示一个非 Null 值。

因此,要知道特定的子条目是否有效,必须对两个有效性位图(结构体数组的位图和子数组的位图)中的相应位进行逻辑与操作。

这在上面的示例中得到了说明,其中一个子数组对于 Null 结构体有一个有效的条目 'alice',但它被结构体数组的有效性位图“隐藏”了。然而,当独立处理时,子数组的相应条目将是非 Null 的。

联合布局#

联合由类型的有序序列定义;联合中的每个槽都可以具有从这些类型中选择的值。这些类型像结构体的字段一样命名,并且名称是类型元数据的一部分。

与其他数据类型不同,联合没有自己的有效性位图。相反,每个槽的 Null 性完全由组成联合的子数组决定。

我们定义了两种不同的联合类型,“稠密”和“稀疏”,它们针对不同的用例进行了优化。

稠密联合#

稠密联合表示一个混合类型数组,每个值有 5 字节的开销。其物理布局如下

  • 每种类型的一个子数组

  • 类型缓冲区:一个 8 位有符号整数缓冲区。联合中的每种类型都有一个相应的类型 ID,其值在此缓冲区中找到。具有超过 128 种可能类型的联合可以建模为联合的联合。

  • 偏移量缓冲区:一个有符号 Int32 值缓冲区,指示给定槽中类型的相应子数组的相对偏移量。每个子值数组的相应偏移量必须是有序/递增的。

布局示例:``DenseUnion<f: Float32, i: Int32>``

对于联合数组

[{f=1.2}, null, {f=3.4}, {i=5}]

将具有以下布局

* Length: 4, Null count: 0
* Types buffer:

  | Byte 0   | Byte 1      | Byte 2   | Byte 3   | Bytes 4-63            |
  |----------|-------------|----------|----------|-----------------------|
  | 0        | 0           | 0        | 1        | unspecified (padding) |

* Offset buffer:

  | Bytes 0-3 | Bytes 4-7   | Bytes 8-11 | Bytes 12-15 | Bytes 16-63           |
  |-----------|-------------|------------|-------------|-----------------------|
  | 0         | 1           | 2          | 0           | unspecified (padding) |

* Children arrays:
  * Field-0 array (f: Float32):
    * Length: 3, Null count: 1
    * Validity bitmap buffer: 00000101

    * Value Buffer:

      | Bytes 0-11     | Bytes 12-63           |
      |----------------|-----------------------|
      | 1.2, null, 3.4 | unspecified (padding) |


  * Field-1 array (i: Int32):
    * Length: 1, Null count: 0
    * Validity bitmap buffer: Not required

    * Value Buffer:

      | Bytes 0-3 | Bytes 4-63            |
      |-----------|-----------------------|
      | 5         | unspecified (padding) |

稀疏联合#

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

虽然稀疏联合可能比稠密联合占用更多的空间,但它在某些用例中可能很理想,因为它具有一些优点

  • 在某些用例中,稀疏联合更适合向量化表达式求值。

  • 通过仅定义类型数组,可以将等长数组解释为联合。

布局示例:``SparseUnion<i: Int32, f: Float32, s: VarBinary>``

对于联合数组

[{i=5}, {f=1.2}, {s='joe'}, {f=3.4}, {i=4}, {s='mark'}]

将具有以下布局

* Length: 6, Null count: 0
* Types buffer:

 | Byte 0     | Byte 1      | Byte 2      | Byte 3      | Byte 4      | Byte 5       | Bytes  6-63           |
 |------------|-------------|-------------|-------------|-------------|--------------|-----------------------|
 | 0          | 1           | 2           | 1           | 0           | 2            | unspecified (padding) |

* Children arrays:

  * i (Int32):
    * Length: 6, Null count: 4
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00010001                 | 0 (padding)           |

    * Value buffer:

      | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-23  | Bytes 24-63           |
      |-------------|-------------|-------------|-------------|-------------|--------------|-----------------------|
      | 5           | unspecified | unspecified | unspecified | 4           |  unspecified | unspecified (padding) |

  * f (Float32):
    * Length: 6, Null count: 4
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00001010                 | 0 (padding)           |

    * Value buffer:

      | Bytes 0-3    | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-23 | Bytes 24-63           |
      |--------------|-------------|-------------|-------------|-------------|-------------|-----------------------|
      | unspecified  | 1.2         | unspecified | 3.4         | unspecified | unspecified | unspecified (padding) |

  * s (`VarBinary`)
    * Length: 6, Null count: 4
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00100100                 | 0 (padding)           |

    * Offsets buffer (Int32)

      | Bytes 0-3  | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-23 | Bytes 24-27 | Bytes 28-63            |
      |------------|-------------|-------------|-------------|-------------|-------------|-------------|------------------------|
      | 0          | 0           | 0           | 3           | 3           | 3           | 7           | unspecified (padding)  |

    * Values buffer:

      | Bytes 0-6  | Bytes 7-63            |
      |------------|-----------------------|
      | joemark    | unspecified (padding) |

只考虑数组中与类型索引对应的槽位。所有“未选择”的值都被忽略,可以是任何语义正确的数组值。

Null 布局#

我们为 Null 数据类型提供了一种简化的、内存高效的布局,其中所有值都是 null。在这种情况下,不分配任何内存缓冲区。

字典编码布局#

字典编码是一种数据表示技术,用于通过整数来表示值,这些整数引用一个通常由唯一值组成的字典。当您具有具有许多重复值的数据时,这可能很有效。

任何数组都可以进行字典编码。字典存储为数组的可选属性。当一个字段被字典编码时,值由一个非负整数数组表示,该数组代表字典中值的索引。字典编码数组的内存布局与原始整数布局的内存布局相同。字典被视为一个单独的列式数组,具有其各自的布局。

例如,您可能有以下数据

type: VarBinary

['foo', 'bar', 'foo', 'bar', null, 'baz']

以字典编码形式,这可能显示为

data VarBinary (dictionary-encoded)
   index_type: Int32
   values: [0, 1, 0, 1, null, 2]

dictionary
   type: VarBinary
   values: ['foo', 'bar', 'baz']

请注意,允许字典包含重复值或 null 值

data VarBinary (dictionary-encoded)
   index_type: Int32
   values: [0, 1, 3, 1, 4, 2]

dictionary
   type: VarBinary
   values: ['foo', 'bar', 'baz', 'foo', null]

此类数组的 null 计数仅由其索引的有效性位图决定,而与其字典中的任何 null 值无关。

由于在某些情况下(例如在 JVM 中)使用无符号整数可能会更困难,我们建议使用带符号整数而不是无符号整数来表示字典索引。此外,我们建议避免使用 64 位无符号整数索引,除非应用程序需要它们。

我们将在下面进一步讨论字典编码与序列化的关系。

行程末尾编码布局#

注意

Arrow 列式格式 1.3 中新增

行程末尾编码 (REE) 是行程长度编码 (RLE) 的一种变体。这些编码非常适合表示包含相同值序列(称为行程)的数据。在行程末尾编码中,每个行程都表示为一个值和一个整数,该整数给出行程在数组中结束的索引。

任何数组都可以进行行程末尾编码。行程末尾编码的数组本身没有缓冲区,但有两个子数组。第一个子数组称为行程末尾数组,包含 16、32 或 64 位带符号整数。每个行程的实际值保存在第二个子数组中。为了确定字段名称和模式,这些子数组分别被指定为run_endsvalues的标准名称。

第一个子数组中的值代表从第一个到当前行程的所有行程的累积长度,即当前行程结束的逻辑索引。这允许使用二进制搜索从逻辑索引进行相对有效的随机访问。单个行程的长度可以通过减去两个相邻的值来确定。(与行程长度编码形成对比,在行程长度编码中,行程的长度被直接表示,并且随机访问效率较低。)

注意

因为 run_ends 子数组不能包含 null,所以考虑为什么 run_ends 是一个子数组而不是像 可变大小列表布局 的偏移量那样的缓冲区是合理的。我们考虑了这种布局,但决定使用子数组。

子数组允许我们将“逻辑长度”(解码后的长度)与父数组关联起来,并将“物理长度”(行程末尾的数量)与子数组关联起来。如果 run_ends 是父数组中的一个缓冲区,那么缓冲区的​​大小将与数组的长度无关,这会造成混淆。

行程的长度必须至少为 1。这意味着行程末尾数组中的值都是正数且严格递增。行程末尾不能为 null。

REE 父对象没有有效性位图,其 null 计数字段应始终为 0。Null 值被编码为值为 null 的行程。

例如,您可能有以下数据

type: Float32
[1.0, 1.0, 1.0, 1.0, null, null, 2.0]

以行程末尾编码形式,这可能显示为

* Length: 7, Null count: 0
* Child Arrays:

  * run_ends (Int32):
    * Length: 3, Null count: 0 (Run Ends cannot be null)
    * Validity bitmap buffer: Not required (if it exists, it should be all 1s)
    * Values buffer

      | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-63           |
      |-------------|-------------|-------------|-----------------------|
      | 4           | 6           | 7           | unspecified (padding) |

  * values (Float32):
    * Length: 3, Null count: 1
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00000101                 | 0 (padding)           |

    * Values buffer

      | Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-63           |
      |-------------|-------------|-------------|-----------------------|
      | 1.0         | unspecified | 2.0         | unspecified (padding) |

每种布局的缓冲区列表#

为避免歧义,我们列出了每种布局的内存缓冲区的顺序和类型。

缓冲区布局#

布局类型

缓冲区 0

缓冲区 1

缓冲区 2

可变缓冲区

原生

有效性

数据

可变二进制

有效性

偏移量

数据

可变二进制视图

有效性

视图

数据

列表

有效性

偏移量

列表视图

有效性

偏移量

大小

固定大小列表

有效性

结构体

有效性

稀疏联合

类型 ID

密集联合体

类型 ID

偏移量

Null

字典编码

有效性

数据(索引)

行程末尾编码

序列化和进程间通信 (IPC)#

列式格式中序列化数据的基本单位是“记录批次”。从语义上讲,记录批次是数组的有序集合,称为其字段,每个数组的长度都相同,但数据类型可能不同。记录批次的字段名称和类型共同构成了批次的模式

在本节中,我们定义了一个协议,用于将记录批次序列化到二进制有效负载流中,并从这些有效负载中重建记录批次,而无需内存复制。

列式 IPC 协议使用以下类型的二进制消息的单向流

  • Schema

  • RecordBatch

  • DictionaryBatch

我们指定了一种所谓的封装 IPC 消息格式,其中包括序列化的 Flatbuffer 类型以及可选的消息正文。在描述如何序列化每种组成 IPC 消息类型之前,我们先定义这种消息格式。

封装消息格式#

对于简单的流式传输和基于文件的序列化,我们为进程间通信定义了一种“封装”的消息格式。可以通过仅检查消息元数据而不必复制或移动任何实际数据,将此类消息“反序列化”为内存中的 Arrow 数组对象。

封装的二进制消息格式如下

  • 一个 32 位续接指示符。值为 0xFFFFFFFF 表示有效消息。此组件在 0.15.0 版本中引入,部分原因是为了解决 Flatbuffers 的 8 字节对齐要求

  • 一个 32 位小端长度前缀,指示元数据大小

  • 消息元数据,使用 Message.fbs 中定义的 Message 类型

  • 填充字节到 8 字节边界

  • 消息正文,其长度必须是 8 字节的倍数

从图示上看,我们有

<continuation: 0xFFFFFFFF>
<metadata_size: int32>
<metadata_flatbuffer: bytes>
<padding>
<message body>

完整的序列化消息的长度必须是 8 字节的倍数,以便消息可以在流之间重新定位。否则,元数据和消息正文之间的填充量可能不确定。

metadata_size 包括 Message 的大小加上填充。 metadata_flatbuffer 包含序列化的 Message Flatbuffer 值,其中内部包含

  • 版本号

  • 特定的消息值(SchemaRecordBatchDictionaryBatch 之一)

  • 消息正文的大小

  • 一个用于任何应用程序提供元数据的 custom_metadata 字段

从输入流读取时,通常首先解析和验证 Message 元数据以获取正文大小。然后可以读取正文。

Schema 消息#

Flatbuffers 文件 Schema.fbs 包含所有内置数据类型的定义以及表示给定记录批次模式的 Schema 元数据类型。模式由字段的有序序列组成,每个字段都有一个名称和类型。序列化的 Schema 不包含任何数据缓冲区,仅包含类型元数据。

Field Flatbuffers 类型包含单个数组的元数据。这包括

  • 字段的名称

  • 字段的数据类型

  • 字段在语义上是否可为空。虽然这对数组的物理布局没有影响,但许多系统会区分可空和不可空字段,我们希望允许它们保留此元数据以实现忠实的模式往返。

  • 一组子 Field 值,用于嵌套类型

  • 一个 dictionary 属性,指示字段是否被字典编码。如果是,则会分配一个字典“id”,以便将后续的字典 IPC 消息与适当的字段匹配。

我们还提供模式级别和字段级别的 custom_metadata 属性,允许系统插入其自己的应用程序定义的元数据,以定制行为。

RecordBatch 消息#

RecordBatch 消息包含与模式确定的物理内存布局相对应的实际数据缓冲区。此消息的元数据提供了每个缓冲区的​​位置和大小,允许使用指针算术重建数组数据结构,从而无需内存复制。

记录批次的序列化形式如下

  • data header,定义为 Message.fbs 中的 RecordBatch 类型。

  • body,一个内存缓冲区的扁平序列,首尾相接,并带有适当的填充以确保至少 8 字节对齐

数据头包含以下内容

  • 记录批次中每个扁平化字段的长度和 null 计数

  • 记录批次主体的每个组成 Buffer 的内存偏移量和长度

字段和缓冲区是通过对记录批次中字段进行前序深度优先遍历来扁平化的。例如,我们考虑模式

col1: Struct<a: Int32, b: List<item: Int64>, c: Float64>
col2: Utf8

扁平化后的版本如下

FieldNode 0: Struct name='col1'
FieldNode 1: Int32 name='a'
FieldNode 2: List name='b'
FieldNode 3: Int64 name='item'
FieldNode 4: Float64 name='c'
FieldNode 5: Utf8 name='col2'

对于产生的缓冲区,我们将有以下内容(参考上表)

buffer 0: field 0 validity
buffer 1: field 1 validity
buffer 2: field 1 values
buffer 3: field 2 validity
buffer 4: field 2 offsets
buffer 5: field 3 validity
buffer 6: field 3 values
buffer 7: field 4 validity
buffer 8: field 4 values
buffer 9: field 5 validity
buffer 10: field 5 offsets
buffer 11: field 5 data

Buffer Flatbuffers 值描述了一块内存的位置和大小。通常,这些是相对于下面定义的封装消息格式解释的。

Buffersize 字段不需要计算填充字节。由于此元数据可用于在库之间传递内存指针地址,因此建议将 size 设置为实际内存大小而不是填充大小。

可变缓冲区#

注意

Arrow 列式格式 1.4 中的新特性

某些类型(如 Utf8View)使用可变数量的缓冲区表示。对于预先排序的逻辑模式中的每个此类字段,variadicBufferCounts 中将有一个条目,指示属于该字段的当前 RecordBatch 中的可变缓冲区数量。

例如,考虑模式

col1: Struct<a: Int32, b: BinaryView, c: Float64>
col2: Utf8View

这有两个带有可变缓冲区的字段,因此在每个 RecordBatch 中,variadicBufferCounts 将有两个条目。对于具有 variadicBufferCounts = [3, 2] 的此模式的 RecordBatch,扁平化的缓冲区将是

buffer 0:  col1    validity
buffer 1:  col1.a  validity
buffer 2:  col1.a  values
buffer 3:  col1.b  validity
buffer 4:  col1.b  views
buffer 5:  col1.b  data
buffer 6:  col1.b  data
buffer 7:  col1.b  data
buffer 8:  col1.c  validity
buffer 9:  col1.c  values
buffer 10: col2    validity
buffer 11: col2    views
buffer 12: col2    data
buffer 13: col2    data

压缩#

记录批次主体缓冲区有三种压缩选项:缓冲区可以未压缩,缓冲区可以与 lz4 压缩编解码器压缩,或者缓冲区可以与 zstd 压缩编解码器压缩。消息正文中扁平化序列中的缓冲区必须使用相同的编解码器单独压缩。序列中特定的缓冲区可以保持未压缩(例如,如果压缩这些特定缓冲区不会显著减小其大小)。

使用的压缩类型在 RecordBatch 消息data header 中可选的 compression 字段中定义,默认值为未压缩。

注意

lz4 压缩编解码器指的是 LZ4 帧格式,不应与“原始”(也称为“块”)格式混淆。

压缩和未压缩缓冲区在序列化形式上的区别如下

  • 如果 RecordBatch 消息 中的缓冲区是压缩的

    • data header 包含记录批次主体中每个压缩缓冲区的长度和内存偏移量以及压缩类型

    • body 包含压缩缓冲区的扁平序列,以及未压缩缓冲区的大小(一个 64 位小端带符号整数),存储在序列中每个缓冲区的前 8 个字节中。可以将此未压缩长度设置为 -1,表示该特定缓冲区保持未压缩。

  • 如果 RecordBatch 消息 中的缓冲区是未压缩的

    • data header 包含记录批次主体中每个未压缩缓冲区的长度和内存偏移量

    • body 包含未压缩缓冲区的扁平序列。

注意

一些 Arrow 实现不支持使用上述一个或两个编解码器生成和消费带有压缩缓冲区的 IPC 数据。有关详细信息,请参阅实现状态

一些应用程序可能会在其存储或传输 Arrow IPC 数据所使用的协议中应用压缩。(例如,HTTP 服务器可能会提供 gzip 压缩的 Arrow IPC 流。)已经在存储或传输协议中使用压缩的应用程序应避免使用缓冲区压缩。双重压缩通常会降低性能,并且不会显著提高压缩率。

字节序(Endianness#

Arrow 格式默认是小端。

序列化的 Schema 元数据有一个 endianness 字段,指示 RecordBatches 的字节序。通常,这是生成 RecordBatch 的系统的字节序。主要用例是在具有相同 Endianness 的系统之间交换 RecordBatches。最初,当尝试读取与底层系统字节序不匹配的 Schema 时,我们将返回一个错误。参考实现侧重于小端,并为此提供了测试。最终,我们可能会通过字节交换提供自动转换。

IPC 流格式#

我们提供了一个记录批次的流协议或“格式”。它被呈现为一系列封装消息,每条消息都遵循上述格式。Schema 位于流中的第一个,并且对于其后的所有记录批次都相同。如果 Schema 中的任何字段是字典编码的,则将包含一个或多个 DictionaryBatch 消息。 DictionaryBatchRecordBatch 消息可以交错,但在 RecordBatch 中使用任何字典键之前,它应该在一个 DictionaryBatch 中定义。

<SCHEMA>
<DICTIONARY 0>
...
<DICTIONARY k - 1>
<RECORD BATCH 0>
...
<DICTIONARY x DELTA>
...
<DICTIONARY y DELTA>
...
<RECORD BATCH n - 1>
<EOS [optional]: 0xFFFFFFFF 0x00000000>

注意

字典批次和记录批次交错的一个边缘情况发生在记录批次包含完全为空的字典编码数组时。在这种情况下,编码列的字典可能会出现在第一个记录批次之后。

当流读取器实现读取流时,在每条消息之后,它可以读取接下来的 8 个字节,以确定流是否继续以及紧随其后的消息元数据的大小。读取消息 flatbuffer 后,您可以读取消息正文。

流写入器可以通过写入 8 个字节来发出流结束 (EOS),这 8 个字节包含 4 字节的续接指示符(0xFFFFFFFF)后跟 0 元数据长度(0x00000000),或者通过关闭流接口来发出流结束 (EOS)。我们推荐使用 “.arrows” 文件扩展名用于流格式,尽管在许多情况下这些流将永远不会作为文件存储。

IPC 文件格式#

我们定义了一种支持随机访问的“文件格式”,它是流格式的扩展。文件以魔术字符串 ARROW1(加上填充)开始和结束。文件中的内容与流格式相同。在文件末尾,我们写入一个页脚,其中包含 Schema 的冗余副本(它是流格式的一部分)以及文件中每个数据块的内存偏移量和大小。这使得可以对文件中的任何记录批次进行随机访问。有关文件页脚的确切详细信息,请参阅 File.fbs

从图示上看,我们有

<magic number "ARROW1">
<empty padding bytes [to 8 byte boundary]>
<STREAMING FORMAT with EOS>
<FOOTER>
<FOOTER SIZE: int32>
<magic number "ARROW1">

在文件格式中,没有要求字典键必须在 DictionaryBatch 中定义,然后才能在 RecordBatch 中使用,只要键在文件中的某个位置定义即可。此外,对于每个字典 ID,只允许有一个非增量字典批次(即不支持字典替换)。增量字典按照它们在文件页脚中出现的顺序应用。我们推荐使用“.arrow”扩展名用于使用此格式创建的文件。请注意,使用此格式创建的文件有时被称为“Feather V2”或使用“.feather”扩展名,名称和扩展名源自“Feather (V1)”,这是 Arrow 项目早期用于 Python (pandas) 和 R 的语言无关的快速数据框存储的概念验证。

字典消息#

字典在流和文件格式中以记录批次的序列形式写入,每个记录批次只有一个字段。因此,一系列记录批次的完整语义模式由 Schema 以及所有字典组成。字典类型在 Schema 中找到,因此有必要首先读取 Schema 以确定字典类型,以便正确解释字典

table DictionaryBatch {
  id: long;
  data: RecordBatch;
  isDelta: boolean = false;
}

消息元数据中的字典 id 可以被 Schema 中引用一次或多次,因此字典甚至可以用于多个字段。有关字典编码数据的语义,请参阅字典编码布局部分。

字典 isDelta 标志允许扩展现有字典,以便为将来的记录批次物化。设置了 isDelta 的字典批次表示其向量应与具有相同 id 的任何先前批次的向量连接起来。在一个对字符串列表 ["A", "B", "C", "B", "D", "C", "E", "A"] 进行编码的流中,使用增量字典批次可能采取以下形式

<SCHEMA>
<DICTIONARY 0>
(0) "A"
(1) "B"
(2) "C"

<RECORD BATCH 0>
0
1
2
1

<DICTIONARY 0 DELTA>
(3) "D"
(4) "E"

<RECORD BATCH 1>
3
2
4
0
EOS

或者,如果将 isDelta 设置为 false,则字典替换具有相同 ID 的现有字典。使用与上面相同的示例,替代编码可以是

<SCHEMA>
<DICTIONARY 0>
(0) "A"
(1) "B"
(2) "C"

<RECORD BATCH 0>
0
1
2
1

<DICTIONARY 0>
(0) "A"
(1) "C"
(2) "D"
(3) "E"

<RECORD BATCH 1>
2
1
3
0
EOS

自定义应用程序元数据#

我们在三个级别提供了一个 custom_metadata 字段,为开发人员提供了一种在 Arrow 协议消息中传递应用程序特定元数据的机制。这包括 FieldSchemaMessage

冒号符号 : 用作命名空间分隔符。它可以在键中多次使用。

ARROW 模式是 custom_metadata 字段中用于内部 Arrow 使用的保留命名空间。例如,ARROW:extension:name

扩展类型#

可以通过在 Field 元数据结构中的 custom_metadata 中设置某些 KeyValue 对来定义用户定义的“扩展”类型。这些扩展键是

  • 'ARROW:extension:name',用于标识自定义数据类型的字符串名称。我们建议您为扩展类型名称使用“命名空间”前缀,以最大限度地减少在同一应用程序中与多个 Arrow 读取器和写入器发生冲突的可能性。例如,使用 myorg.name_of_type 而不是简单地使用 name_of_type

  • 'ARROW:extension:metadata',用于 ExtensionType 的序列化表示,这对于重建自定义类型是必要的

注意

arrow. 开头的扩展名称保留给规范扩展类型,不应用于第三方扩展类型。

此扩展元数据可以注释任何内置 Arrow 逻辑类型。例如,Arrow 指定了一种规范扩展类型,它将 UUID 表示为 FixedSizeBinary(16)。Arrow 实现不要求支持规范扩展,因此不支持此 UUID 类型的实现将简单地将其解释为 FixedSizeBinary(16),并在后续的 Arrow 协议消息中传递 custom_metadata

扩展类型可以使用也可能不使用 'ARROW:extension:metadata' 字段。让我们考虑一些示例扩展类型

  • uuid 表示为 FixedSizeBinary(16),元数据为空

  • latitude-longitude 表示为 struct<latitude: double, longitude: double>,元数据为空

  • tensor(多维数组)存储为 Binary 值,并具有指示每个值的数据类型和形状的序列化元数据。这可能类似于 JSON,例如 {'type': 'int8', 'shape': [4, 5]},代表一个 4x5 单元张量。

  • trading-time 表示为 Timestamp,具有指示数据所对应市场交易日历的序列化元数据

另请参阅

规范扩展类型

实施指南#

执行引擎(或框架、或 UDF 执行器、或存储引擎等)可以只实现 Arrow 规范的子集,并且/或者在给定以下约束的情况下对其进行扩展

实现规范的子集#

  • 如果只生成(而不是消费)箭头向量:可以实现向量规范和相应元数据的任何子集。

  • 如果消费和生成向量:至少需要支持一个最小向量子集。生成向量及其相应元数据的子集始终是可行的。对向量的消费应至少将不受支持的输入向量转换为受支持的子集(例如,Timestamp.millis 转换为 timestamp.micros 或 int32 转换为 int64)。

可扩展性#

执行引擎实现者也可以在内部用自己的向量扩展其内存表示,只要它们不暴露出来即可。在将数据发送到期望 Arrow 数据的另一个系统之前,这些自定义向量应转换为 Arrow 规范中存在的类型。