箭头列式格式#

版本:1.5

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

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

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

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

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

  • SIMD 和向量化友好

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

箭头列式格式以相对较高的变异操作成本换取分析性能和数据局部性保证。本文档仅关注内存中数据表示和序列化细节;诸如协调数据结构变异等问题留由实现来处理。

术语#

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

  • **数组**或**向量**:具有已知长度的一系列值,所有值都具有相同的类型。这些术语在不同的 Arrow 实现中可以互换使用,但在本文档中我们使用“数组”。

  • **槽**:某个特定数据类型数组中的单个逻辑值

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

  • **物理布局**:数组的基础内存布局,不考虑任何值语义。例如,32 位有符号整数数组和 32 位浮点数数组具有相同的布局。

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

  • **基本类型**:没有子类型的类型。这包括固定位宽、可变大小二进制和空类型等类型。

  • **嵌套类型**:其完整结构依赖于一个或多个其他子类型的类型。只有当它们的子类型相等时,两个完全指定的嵌套类型才相等。例如,List<U>List<V> 不同的充分必要条件是 U 和 V 是不同的类型。

  • **父**和**子数组**:用于表达嵌套类型结构中物理值数组之间关系的名称。例如,List<T> 类型的父数组具有 T 类型的数组作为其子数组(有关列表的更多信息,请参见下文)。

  • **参数化类型**:需要其他参数才能完全确定其语义的类型。例如,所有嵌套类型都是通过构造参数化的。时间戳也是参数化的,因为它需要一个单位(例如微秒)和一个时区。

数据类型#

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

Schema.fbs 是标准箭头数据类型描述的权威来源。但是,我们也为了方便起见提供了下表

类型

类型参数 (1)

物理内存布局

布尔

固定大小基本类型

整数

  • 位宽

  • 有符号性

“ (与上面相同)

浮点数

  • 精度

十进制

  • 位宽

  • 刻度

  • 精度

日期

  • 单位

时间

  • 位宽 (2)

  • 单位

时间戳

  • 单位

  • 时区

间隔

  • 单位

持续时间

  • 单位

固定大小二进制

  • 字节宽

固定大小二进制

二进制

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

Utf8

大二进制

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

大 Utf8

二进制视图

可变大小二进制视图

Utf8 视图

固定大小列表

  • 值类型

  • 列表大小

固定大小列表

列表

  • 值类型

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

大列表

  • 值类型

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

列表视图

  • 值类型

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

大列表视图

  • 值类型

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

结构体

  • 子项

结构体

映射

  • 子项

  • 键排序

结构体的可变大小列表

联合

  • 子项

  • 模式

  • 类型 ID

密集或稀疏联合 (3)

字典

  • 索引类型 (4)

  • 值类型

  • 有序性

字典编码

运行结束编码

  • 运行结束类型 (5)

  • 值类型

运行结束编码

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

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

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

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

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

注意

有时术语“逻辑类型”用于表示箭头数据类型,并将它们与各自的物理布局区分开来。但是,与其他类型系统(如 Apache Parquet)不同,箭头类型系统没有物理类型和逻辑类型的单独概念。

箭头类型系统单独提供 扩展类型,它允许使用更丰富的面向应用程序的语义来注释标准箭头数据类型(例如,在标准字符串数据类型上定义“JSON”类型)。

物理内存布局#

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

  • 一种数据类型。

  • 一系列缓冲区。

  • 长度作为 64 位有符号整数。允许实现限制为 32 位长度,有关详细信息,请参见下文。

  • 空计数作为 64 位有符号整数。

  • 对于字典编码数组,可选的**字典**。

嵌套数组另外还具有一系列一个或多个这些项目的集合,称为**子数组**。

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

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

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

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

  • **固定大小列表**:嵌套布局,其中每个值都具有从子数据类型中获取的相同数量的元素。

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

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

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

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

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

  • **运行结束编码 (REE)**:嵌套布局,由两个子数组组成,一个表示值,另一个表示对应值运行在逻辑上的索引结束位置。

  • **空**:一系列所有空值。

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

缓冲区对齐和填充#

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

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

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

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

64 字节对齐的建议来自英特尔性能指南,该指南建议将内存对齐以匹配 SIMD 寄存器宽度。选择特定的填充长度是因为它与广泛部署的 x86 架构(英特尔 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)

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

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

通常,偏移量数组中的第一个槽为 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 数据接口中存储可变缓冲区长度。

可变大小列表布局#

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

列表布局#

List 布局由两个缓冲区(有效性位图和偏移量缓冲区)以及一个子数组定义。偏移量与可变大小二进制情况下的相同,并且 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) |

ListView 布局#

Arrow 版本中的新增功能: 列式格式 1.4

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

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

每个 ListView 值,包括空值,都必须保证以下不变性

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

ListView 类型指定为 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: 4, 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) |

结构体有效性#

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

因此,要了解特定子条目是否有效,必须将两个有效性位图(结构体数组的和子数组的)中相应位的逻辑 AND 取值。

这在上例中得到了说明,其中一个子数组对于空结构体具有有效条目 '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_ends**和**values**。

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

注意

由于run_ends子数组不能有空值,因此有必要考虑为什么run_ends是子数组而不是像Variable-size List Layout的偏移量那样只是一个缓冲区。这种布局也考虑过,但最终决定使用子数组。

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

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

REE父级没有有效性位图,并且它的空值计数字段应始终为0。空值被编码为值为null的运行。

例如,您可以拥有以下数据

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

以Run-end-encoded形式,它可能显示为

* 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

偏移量

字典编码

有效性

数据(索引)

运行结束编码

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

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

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

列式IPC协议利用这些类型的二进制消息的一向流

  • 模式

  • 记录批

  • 字典批

我们指定了一个所谓的封装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属性,允许系统插入他们自己的应用程序定义的元数据以自定义行为。

记录批消息#

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

记录批的序列化形式如下

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

  • body,一个按端到端写入的内存缓冲区的扁平序列,并进行适当的填充以确保至少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)使用可变数量的缓冲区表示。对于预排序扁平化逻辑模式中的每个此类Field,variadicBufferCounts中将有一个条目,以指示属于当前RecordBatch中该Field的可变参数缓冲区的数量。

例如,考虑模式

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

它有两个具有可变参数缓冲区的字段,因此variadicBufferCounts在每个RecordBatch中将有两个条目。对于具有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包含**压缩缓冲区**的扁平序列以及**未压缩缓冲区的长度**,作为存储在序列中每个缓冲区前 8 个字节中的 64 位小端序有符号整数。此未压缩长度可以设置为-1以指示该特定缓冲区保持未压缩状态。

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

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

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

注意

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

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

字节序(Endianness#

Arrow 格式默认情况下是小端序。

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

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>

注意

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

当流读取器实现读取流时,在每条消息之后,它可以读取接下来的 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设置的字典批次表示其向量应与任何先前具有相同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值,并具有序列化元数据,指示每个值的类型和形状。这可以是类似{'type': 'int8', 'shape': [4, 5]}的 JSON,用于 4x5 单元格张量。

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

另请参阅

规范扩展类型

实现指南#

执行引擎(或框架、或 UDF 执行器、或存储引擎等)只能实现 Arrow 规范的一个子集,或者在以下约束条件下扩展它

实现规范的子集#

  • **如果仅生成(而不是使用)Arrow 向量**:可以实现向量规范的任何子集和相应的元数据。

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

可扩展性#

执行引擎实现者还可以使用他们自己的向量在内部扩展其内存表示,只要它们从未公开过。在将数据发送到另一个期望 Arrow 数据的系统之前,应将这些自定义向量转换为 Arrow 规范中存在的类型。