Arrow 列式格式#

版本:1.5

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

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

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

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

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

  • 对 SIMD 和矢量化友好

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

Arrow 列式格式提供了分析性能和数据局部性保证,但代价是相对昂贵的修改操作。本文档仅关注内存中的数据表示和序列化细节;诸如协调数据结构修改等问题留给具体实现来处理。

术语#

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

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

  • Slot (槽):某种特定数据类型的数组中的单个逻辑值

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

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

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

  • Primitive type (原生类型):没有子类型的数据类型。这包括固定位宽、可变大小二进制和空类型等。

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

  • Parent and 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) 时间类型的位宽参数在技术上是多余的,因为每个单位都强制要求一个单一的位宽。

  • (3) 联合体类型是使用稀疏还是密集布局由其模式参数表示。

  • (4) 字典类型的索引类型只能是整数类型,最好是有符号整数,宽度为 8 到 64 位。

  • (5) 游程编码类型的游程结束类型只能是有符号整数类型,宽度为 16 到 64 位。

注意

有时,“逻辑类型”一词用于表示 Arrow 数据类型,并将其与各自的物理布局区分开来。然而,与其他类型系统(如 Apache Parquet)不同,Arrow 类型系统没有物理类型和逻辑类型的独立概念。

Arrow 类型系统另外提供了扩展类型,允许用更丰富的面向应用的语义来注解标准的 Arrow 数据类型(例如,在标准字符串数据类型之上定义一个“JSON”类型)。

物理内存布局#

数组由几部分元数据和数据定义

  • 一个数据类型。

  • 一系列缓冲区。

  • 一个 64 位有符号整数表示的长度。允许实现仅限于 32 位长度,下文将详细说明。

  • 一个 64 位有符号整数表示的空值计数。

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

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

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

  • 原生(定长):一系列值,每个值具有相同的字节或位宽。

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

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

  • 定长列表:一种嵌套布局,其中每个值具有相同数量的元素,这些元素取自子数据类型。

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

  • 可变大小列表视图:一种嵌套布局,其中每个值是一个可变长度的值序列,这些值取自子数据类型。此布局与可变大小列表不同,它有一个额外的缓冲区,包含每个列表值的大小。这消除了对偏移量缓冲区的约束——它不需要按顺序排列。

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

  • 稀疏密集联合体:一种嵌套布局,表示一系列值,每个值都可以从一组子数组类型中选择其类型。

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

  • 游程编码 (REE):一种嵌套布局,由两个子数组组成,一个表示值,另一个表示相应值的游程结束的逻辑索引。

  • 空值:一个全是空值的序列。

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

缓冲区对齐与填充#

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

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

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

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

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

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

数组长度#

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

空值计数#

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

有效性位图#

数组中的任何值,无论是原生类型还是嵌套类型,在语义上都可以为空。

除联合体类型(稍后会详细介绍)外,所有数组类型都利用一个专用的内存缓冲区,称为有效性(或“空值”)位图,来编码每个值槽的空或非空状态。有效性位图必须足够大,至少为每个数组槽提供 1 位。

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

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

空值计数为 0 的数组可以选择不分配有效性位图;如何表示这一点取决于实现(例如,C++ 实现可能使用 NULL 指针来表示这种“不存在的”有效性位图)。为了方便,实现也可以选择总是分配一个有效性位图。Arrow 数组的使用者应该准备好处理这两种可能性。

嵌套类型数组(如上所述,联合体类型除外)拥有其自己的顶层有效性位图和空值计数,无论其子数组的空值计数和有效位如何。

空值的数组槽不需要具有特定的值;任何“被屏蔽”的内存可以有任何值,不需要清零,尽管实现通常会选择将空值处的内存清零。

定长原生布局#

原生值数组表示一个由值组成的数组,每个值具有相同的物理槽位宽度,通常以字节为单位,尽管规范也提供了位打包类型(例如,用位编码的布尔值)。

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

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

布局示例: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) |

布局示例:非空 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 个或多个字节组成。原生数组只有一个值缓冲区,而可变大小二进制则有一个偏移量缓冲区和一个数据缓冲区。

偏移量缓冲区包含 length + 1 个有符号整数(32 位或 64 位,取决于数据类型),它们编码了每个槽在数据缓冲区中的起始位置。每个槽中值的长度是通过该槽索引处的偏移量与下一个偏移量之间的差值计算得出的。例如,槽 j 的位置和长度计算如下

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

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

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

通常,偏移量数组中的第一个槽是 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 个或多个字节组成。这些字节的位置由一个视图缓冲区指示,该缓冲区可能指向多个数据缓冲区之一,或者可能内联包含字符。

视图缓冲区包含 length 个视图结构,布局如下

* 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      |

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

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

在长字符串的情况下,一个缓冲区索引指示哪个数据缓冲区存储数据字节,一个偏移量指示数据字节在该缓冲区中的起始位置。缓冲区索引 0 指的是第一个数据缓冲区,即有效性缓冲区和视图缓冲区之后的第一个缓冲区。半开区间 [offset, offset + length) 必须完全包含在指定的缓冲区内。字符串前四个字节的副本以内联方式存储在前缀中,紧跟在长度之后。这个前缀为字符串比较提供了一个有益的快速路径,因为比较结果通常在前四个字节内就能确定。

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

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

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

可变大小列表布局#

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

列表布局#

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

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

列表类型指定为 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 新增

列表视图布局由三个缓冲区定义:一个有效性位图,一个偏移量缓冲区和一个额外的大小缓冲区。大小和偏移量具有相同的位宽,支持 32 位和 64 位有符号整数选项。

与列表布局一样,偏移量编码了每个槽在子数组中的起始位置。与列表布局不同的是,列表长度显式地存储在大小缓冲区中,而不是推断出来的。这允许偏移量是无序的。子数组的元素不必按照它们在父数组的列表元素中逻辑上出现的顺序存储。

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

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 的值存储在值数组的一个长度为 N 的切片中,起始偏移量为 j * 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) |

结构体有效性#

结构体数组有自己的有效性位图,它独立于其子数组的有效性位图。当结构体数组的有效性位图指示一个空值时,其一个或多个子数组在对应槽中可能有一个非空值;反之,子数组的有效性位图可能指示一个空值,而结构体数组的有效性位图显示一个非空值。

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

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

联合体布局#

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

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

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

密集联合体#

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

  • 每种类型一个子数组

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

  • 偏移量缓冲区:一个有符号 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 数据类型提供了一种简化的内存高效布局,其中所有值都为空。在这种情况下,不分配任何内存缓冲区。

字典编码布局#

字典编码是一种数据表示技术,通过引用一个通常由唯一值组成的字典的整数来表示值。当数据中包含许多重复值时,这种方法非常有效。

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

举个例子,你可能有以下数据

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']

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

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

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

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

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

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

游程编码布局#

注意

Arrow 列式格式 1.3 新增

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

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

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

注意

因为 run_ends 子数组不能有空值,所以有理由考虑为什么 run_ends 是一个子数组而不是像 可变大小列表布局 的偏移量那样的缓冲区。这种布局被考虑过,但最终决定使用子数组。

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

一个游程的长度必须至少为 1。这意味着游程结束数组中的所有值都是正数且严格递增。游程结束不能为空。

REE 父数组没有有效性位图,其空值计数字段应始终为 0。空值被编码为值为 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 元数据以获取消息体大小。然后可以读取消息体。

模式消息#

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

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

  • 字段的名称

  • 字段的数据类型

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

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

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

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

RecordBatch 消息#

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

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

  • 数据头,在 Message.fbs 中定义为 RecordBatch 类型。

  • 主体,一个扁平的内存缓冲区序列,端到端地写入,并带有适当的填充以确保至少 8 字节对齐

数据头包含以下内容

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

  • 记录批次主体中每个组成 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

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

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 消息数据头中,位于可选的compression字段中定义,默认为不压缩。

注意

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

序列化形式中压缩和未压缩缓冲区之间的区别如下

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

    • 数据头 包括记录批次主体中每个压缩缓冲区的长度和内存偏移量,以及压缩类型

    • 主体 包括一个扁平的压缩缓冲区序列,以及作为序列中每个缓冲区前 8 个字节存储的 64 位小端序有符号整数的未压缩缓冲区长度。这个未压缩长度可以设置为 -1,以表示该特定缓冲区未被压缩。

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

    • 数据头 包括记录批次主体中每个未压缩缓冲区的长度和内存偏移量

    • 主体 包括一个扁平的未压缩缓冲区序列。

注意

一些 Arrow 实现缺乏对使用上述一种或两种编解码器生成和消费带压缩缓冲区的 IPC 数据的支持。详情请参见实现状态

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

字节序 (Endianness)#

Arrow 格式默认是小端序。

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

IPC 流格式#

我们为记录批次提供了一种流协议或“格式”。它表现为一系列封装的消息,每个消息都遵循上述格式。模式首先出现在流中,并且对于之后的所有记录批次都是相同的。如果模式中的任何字段是字典编码的,则会包含一个或多个 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,您就可以读取消息体了。

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

IPC 文件格式#

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

示意图如下

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

在文件格式中,不要求字典键必须在 RecordBatch 中使用之前在 DictionaryBatch 中定义,只要这些键在文件中的某个地方被定义即可。此外,每个字典 ID 拥有多个非增量字典批次是无效的(即不支持字典替换)。增量字典按它们在文件页脚中出现的顺序应用。我们建议对使用此格式创建的文件使用“.arrow”扩展名。请注意,使用此格式创建的文件有时被称为“Feather V2”或使用“.feather”扩展名,该名称和扩展名源于“Feather (V1)”,这是 Arrow 项目早期的一个概念验证,用于为 Python (pandas) 和 R 提供与语言无关的快速数据帧存储。

字典消息#

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

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

消息元数据中的字典 id 可以在模式中被引用一次或多次,这样字典甚至可以用于多个字段。有关字典编码数据的语义的更多信息,请参见字典编码布局部分。

字典 isDelta 标志允许为将来的记录批次实例化扩展现有字典。一个 isDelta 设置为 true 的字典批次表示其向量应与具有相同 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' 字段。让我们考虑一些扩展类型的例子

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

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

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

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

另请参阅

规范扩展类型

实现指南#

执行引擎(或框架、UDF 执行器、存储引擎等)可以仅实现 Arrow 规范的子集和/或在以下约束条件下进行扩展

实现规范的子集#

  • 如果只生产(而不消费)Arrow 向量:可以实现向量规范的任何子集以及相应的元数据。

  • 如果既消费又生产向量:需要支持一个最小的向量子集。生产向量的子集及其相应的元数据总是可以的。消费向量时,至少应将不支持的输入向量转换为支持的子集(例如,Timestamp.millis 到 timestamp.micros 或 int32 到 int64)。

可扩展性#

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