Arrow 列式格式#
版本:1.5
Arrow 列式格式包含一个语言无关的内存数据结构规范、元数据序列化以及用于序列化和通用数据传输的协议。
本文档旨在提供足够的细节,以便在没有现有实现的情况下创建列式格式的新实现。我们使用 Google 的 Flatbuffers 项目进行元数据序列化,因此在阅读本文档时有必要参考该项目的 Flatbuffers 协议定义文件。
列式格式具有一些关键特性
数据邻接,便于顺序访问(扫描)
O(1)(常数时间)随机访问
对 SIMD 和向量化友好
可重定位而无需“指针调整”(pointer swizzling),可在共享内存中实现真正的零拷贝访问
Arrow 列式格式以相对较高的修改操作成本为代价,提供了分析性能和数据局部性保证。本文档仅关注内存数据表示和序列化细节;数据结构的修改协调等问题留给实现来处理。
术语#
由于不同的项目使用不同的词汇来描述各种概念,这里有一个小词汇表来帮助消除歧义。
数组 (Array) 或 向量 (Vector):具有已知长度且所有值类型相同的序列。这些术语在不同的 Arrow 实现中可互换使用,但在本文档中我们使用“数组”。
槽位 (Slot):数组中某种特定数据类型的单个逻辑值
缓冲区 (Buffer) 或 连续内存区域 (Contiguous memory region):具有给定长度的连续虚拟地址空间。可以通过小于该区域长度的单个指针偏移量到达任何字节。
物理布局 (Physical Layout):不考虑任何值语义的数组底层内存布局。例如,32 位有符号整数数组和 32 位浮点数组具有相同的布局。
数据类型 (Data type):面向应用程序的语义值类型,使用某种物理布局实现。例如,Decimal128 值以 16 个字节存储在固定大小的二进制布局中。时间戳可以存储为 64 位固定大小的布局。
原始类型 (Primitive type):没有子类型的类型。这包括固定位宽、可变大小二进制和 null 类型等类型。
嵌套类型 (Nested type):其完整结构依赖于一个或多个其他子类型的类型。当且仅当它们的子类型相等时,两个完全指定的嵌套类型才相等。例如,
List<U>
与List<V>
不同当且仅当 U 和 V 是不同的类型。父数组 (Parent array) 和 子数组 (child arrays):用于表达嵌套类型结构中物理值数组之间关系的名称。例如,
List<T>
类型的父数组将其子数组作为 T 类型数组(有关列表的更多信息,请参见下文)。参数化类型 (Parametric type):需要额外参数才能完全确定其语义的类型。例如,所有嵌套类型都是通过构造参数化的。时间戳也是参数化的,因为它需要一个单位(例如微秒)和一个时区。
数据类型#
文件 Schema.fbs 定义了 Arrow 列式格式支持的内置数据类型。每种数据类型都使用明确定义的物理布局。
Schema.fbs 是描述标准 Arrow 数据类型的权威来源。但是,为了方便起见,我们也提供了下表
类型 |
类型参数 (1) |
物理内存布局 |
---|---|---|
Null |
Null |
|
Boolean |
Fixed-size Primitive |
|
Int |
|
“ (同上) |
Floating Point |
|
“ |
Decimal |
|
“ |
Date |
|
“ |
Time |
|
“ |
Timestamp |
|
“ |
Interval |
|
“ |
Duration |
|
“ |
Fixed-Size Binary |
|
Fixed-size Binary |
Binary |
Variable-size Binary with 32-bit offsets |
|
Utf8 |
“ |
|
Large Binary |
Variable-size Binary with 64-bit offsets |
|
Large Utf8 |
“ |
|
Binary View |
Variable-size Binary View |
|
Utf8 View |
“ |
|
Fixed-Size List |
|
Fixed-size List |
List |
|
Variable-size List with 32-bit offsets |
Large List |
|
Variable-size List with 64-bit offsets |
List View |
|
Variable-size List View with 32-bit offsets and sizes |
Large List View |
|
Variable-size List View with 64-bit offsets and sizes |
Struct |
|
Struct |
Map |
|
Variable-size List of Structs |
Union |
|
Dense or Sparse Union (3) |
Dictionary |
|
Dictionary Encoded |
Run-End Encoded |
|
Run-End Encoded |
(1) 斜体 列出的类型参数表示该数据类型的子类型。
(2) Time 类型的 位宽 参数在技术上是冗余的,因为每个 单位 都规定了一个固定的位宽。
(3) Union 类型使用 Sparse 布局还是 Dense 布局由其 mode 参数表示。
(4) Dictionary 类型的 index type 只能是整数类型,最好是有符号整数,位宽为 8 到 64 位。
(5) Run-End Encoded 类型的 run end type 只能是有符号整数类型,位宽为 16 到 64 位。
注意
有时使用术语“逻辑类型”(logical type)来表示 Arrow 数据类型,并将其与各自的物理布局区分开来。然而,与 Apache Parquet 等其他类型系统不同,Arrow 类型系统没有独立的物理类型和逻辑类型概念。
Arrow 类型系统另外提供了 扩展类型,允许使用更丰富的面向应用程序的语义来标注标准 Arrow 数据类型(例如,在标准 String 数据类型上定义一个“JSON”类型)。
物理内存布局#
数组由几个元数据和数据片段定义
数据类型。
缓冲区序列。
一个 64 位有符号整数表示的长度。允许实现受限于 32 位长度,详见下文。
一个 64 位有符号整数表示的 null 计数。
可选的 字典,用于字典编码数组。
嵌套数组另外包含一个或多个这些项的集合序列,称为 子数组。
每种数据类型都有明确定义的物理布局。以下是 Arrow 定义的不同物理布局
Primitive (fixed-size):每个值具有相同字节或位宽度的值序列,通常以字节为单位测量,但也规定了位打包类型(例如,以位编码的布尔值)。
Variable-size Binary:每个值具有可变字节长度的值序列。支持使用 32 位和 64 位长度编码的两种变体。
View of Variable-size Binary:每个值具有可变字节长度的值序列。与 Variable-size Binary 不同,这种布局的值分布在可能多个缓冲区中,而不是紧密地顺序打包在单个缓冲区中。
Fixed-size List:一种嵌套布局,其中每个值都具有从子数据类型中获取的相同数量的元素。
Variable-size List:一种嵌套布局,其中每个值都是从子数据类型中获取的可变长度值序列。支持使用 32 位和 64 位长度编码的两种变体。
View of Variable-size List:一种嵌套布局,其中每个值都是从子数据类型中获取的可变长度值序列。这种布局与 Variable-size List 的区别在于,它有一个额外的缓冲区包含每个列表值的大小。这消除了对 offsets 缓冲区的约束——它不需要有序。
Struct:一种嵌套布局,由一组命名子 字段 组成,每个字段长度相同但类型可能不同。
Sparse 和 Dense Union:一种嵌套布局,表示一个值序列,其中每个值可以从子数组类型集合中选择类型。
Dictionary-Encoded:一种布局,由一个整数序列(任何位宽)组成,这些整数表示指向可以任何类型的字典的索引。
Run-End Encoded (REE):一种嵌套布局,由两个子数组组成,一个表示值,另一个表示相应值运行结束的逻辑索引。
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,我们建议将长度限制在 231 - 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 值的内存归零。
固定大小原始布局#
原始值数组表示值数组,每个值具有相同的物理槽位宽度,通常以字节为单位测量,尽管规范也提供了位打包类型(例如,以位编码的布尔值)。
在内部,数组包含一个连续的内存缓冲区,其总大小至少与槽位宽度乘以数组长度一样大。对于位打包类型,大小向上取整到最近的字节。
相关的有效性位图是连续分配的(如上所述),但不需要与 values 缓冲区在内存中相邻。
示例布局: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 个或更多字节。原始数组有一个 values 缓冲区,而可变大小二进制数组有一个 offsets 缓冲区和 data 缓冲区。
offsets 缓冲区包含 length + 1
个有符号整数(取决于数据类型,可以是 32 位或 64 位),它们编码每个槽位在 data 缓冲区中的起始位置。每个槽位中值的长度使用该槽位索引处的偏移量与后续偏移量之间的差值计算。例如,槽位 j 的位置和长度计算如下
slot_position = offsets[j]
slot_length = offsets[j + 1] - offsets[j] // (for 0 <= j < length)
需要注意的是,null 值可能具有正的槽位长度。也就是说,null 值可能在 data 缓冲区中占用 非空 的内存空间。在这种情况下,相应内存空间的内容是未定义的。
Offsets 必须单调递增,即对于 0 <= j < length
,必须有 offsets[j+1] >= offsets[j]
,即使是 null 槽位也是如此。此属性确保所有值的位置都是有效且明确定义的。
通常 offsets 数组的第一个槽位是 0,最后一个槽位是 values 数组的长度。序列化此布局时,建议将 offsets 归一化为从 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 个或更多字节。这些字节的位置通过一个 views 缓冲区指示,该缓冲区可以指向潜在的多个 data 缓冲区中的一个,或者包含内联的字符。
views 缓冲区包含 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
填充。
在长字符串情况下,一个缓冲区索引指示哪个 data 缓冲区存储数据字节,一个偏移量指示数据字节在该缓冲区中的起始位置。缓冲区索引 0 指的是第一个 data 缓冲区,即有效性缓冲区和 views 缓冲区 之后 的第一个缓冲区。半开区间 [offset, offset + length)
必须完全包含在指定的缓冲区内。字符串的前四个字节的副本以内联方式存储在长度之后的 prefix 中。此 prefix 有助于实现字符串比较的高效快速路径,因为字符串比较通常在前四个字节内就能确定结果。
所有整数(长度、缓冲区索引和偏移量)都是有符号的。
此布局改编自慕尼黑工业大学的 UmbraDB。
请注意,此布局在 Arrow C 数据接口 中使用一个附加缓冲区来存储可变长缓冲区的长度。
可变大小列表布局#
列表是一种嵌套类型,在语义上类似于可变大小二进制。有两种列表布局变体——“list”和“list-view”——每种变体都可以通过 32 位或 64 位偏移量整数来定界。
列表布局#
List 布局由两个缓冲区(有效性位图和 offsets 缓冲区)和一个子数组定义。Offsets 与可变大小二进制情况中的相同,并且支持 32 位和 64 位有符号整数偏移量作为选项。与引用附加 data 缓冲区不同,这些 offsets 引用子数组。
类似于可变大小二进制的布局,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) |
ListView 布局#
Arrow 新增版本: 列式格式 1.4
ListView 布局由三个缓冲区定义:有效性位图、offsets 缓冲区和一个附加的 sizes 缓冲区。Sizes 和 offsets 具有相同的位宽度,并且支持 32 位和 64 位有符号整数选项。
与 List 布局一样,offsets 编码每个槽位在子数组中的起始位置。与 List 布局不同,列表长度明确存储在 sizes 缓冲区中,而不是推断。这允许 offsets 无序。子数组的元素不必按照它们在父数组的列表元素中逻辑出现的顺序存储。
每个 list-view 值,包括 null 值,都必须保证以下不变式
0 <= offsets[i] <= length of the child array
0 <= offsets[i] + size[i] <= length of the child array
list-view 类型指定为 ListView<T>
,其中 T
是任何类型(原始类型或嵌套类型)。在这些示例中,我们使用 32 位 offsets 和 sizes,而 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) |
固定大小列表布局#
Fixed-Size List 是一种嵌套类型,其中每个数组槽位都包含固定大小的值序列,这些值都具有相同的类型。
固定大小列表类型指定为 FixedSizeList<T>[N]
,其中 T
是任何类型(原始类型或嵌套类型),N
是一个 32 位有符号整数,表示列表的长度。
固定大小列表数组由一个 values 数组表示,该数组是类型 T 的子数组。T 也可以是嵌套类型。固定大小列表数组槽位 j
中的值存储在 values 数组的一个长度为 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 |
Struct 布局#
Struct 是一种嵌套类型,由有序的类型序列(它们都可以不同)参数化,这些类型称为其字段。每个字段必须有一个 UTF8 编码的名称,并且这些字段名称是类型元数据的一部分。
在物理上,Struct 数组为每个字段都有一个子数组。子数组是独立的,在内存中不需要彼此相邻。Struct 数组还有一个有效性位图来编码顶层有效性信息。
例如,Struct(此处字段名称以字符串形式表示以便说明)
Struct <
name: VarBinary
age: Int32
>
有两个子数组,一个 VarBinary
数组(使用可变大小二进制布局)和一个 4 字节原始值数组,其逻辑类型为 Int32
。
示例布局:``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) |
Struct 有效性#
Struct 数组拥有自己的有效性位图,独立于其子数组的有效性位图。Struct 数组的有效性位图可能在一个或多个子数组在其对应槽位具有非 null 值时指示 null;反之,子数组的有效性位图可能指示 null,而 Struct 数组的有效性位图显示非 null 值。
因此,要确定特定子条目是否有效,必须对两个有效性位图(Struct 数组的和子数组的)中的相应位进行逻辑 AND 运算。
这在上面的示例中有所体现,其中一个子数组在 null struct 中有一个有效条目 'alice'
,但被 Struct 数组的有效性位图“隐藏”了。然而,当独立处理时,子数组的相应条目将是非 null 的。
联合布局#
联合由有序的类型序列定义;联合中的每个槽位都可以选择这些类型中的一个值。这些类型像 Struct 的字段一样命名,名称是类型元数据的一部分。
与其他数据类型不同,联合没有自己的有效性位图。相反,每个槽位的 null 性完全由构成联合的子数组确定。
我们定义了两种不同的联合类型,“dense”和“sparse”,它们针对不同的用例进行了优化。
稠密联合#
稠密联合表示一个混合类型数组,每个值有 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 布局#
我们为所有值都是 null 的 Null 数据类型提供了一种简化的内存高效布局。在这种情况下,不会分配内存缓冲区。
字典编码布局#
字典编码是一种数据表示技术,通过引用通常由唯一值组成的 dictionary 的整数来表示值。当数据包含许多重复值时,这种方法非常有效。
任何数组都可以进行字典编码。字典作为数组的一个可选属性存储。当字段进行字典编码时,值由表示字典中值索引的非负整数数组表示。字典编码数组的内存布局与原始整数布局相同。字典作为一个单独的列式数组处理,具有其各自的布局。
例如,您可能拥有以下数据:
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]
此类数组的空值计数仅由其索引的有效性位图决定,而与字典中的任何空值无关。
由于无符号整数在某些情况下(例如在 JVM 中)可能更难以使用,因此我们建议优先使用有符号整数而不是无符号整数来表示字典索引。此外,我们建议避免使用 64 位无符号整数索引,除非应用程序确实需要它们。
我们将在下方进一步讨论字典编码与序列化的关系。
运行结束编码布局#
Arrow 格式 1.3 版本新增:列式格式 1.3
运行结束编码(REE)是游程编码(RLE)的一种变体。这些编码非常适合表示包含相同值序列(称为运行)的数据。在运行结束编码中,每个运行表示为一个值和一个整数,该整数给出运行结束的数组中的索引。
任何数组都可以进行运行结束编码。运行结束编码数组本身没有缓冲区,但有两个子数组。第一个子数组称为运行结束数组,包含 16、32 或 64 位有符号整数。每个运行的实际值存储在第二个子数组中。为了确定字段名称和模式,这些子数组分别被规定为标准名称 run_ends 和 values。
第一个子数组中的值表示从第一个运行到当前运行的所有运行的累积长度,即当前运行结束的逻辑索引。这允许使用二分查找从逻辑索引进行相对高效的随机访问。可以通过减去两个相邻值来确定单个运行的长度。(与游程编码形成对比,游程编码直接表示运行的长度,并且随机访问效率较低。)
注意
由于 run_ends
子数组不能包含 null 值,因此考虑为什么 run_ends
是一个子数组而不仅仅是一个缓冲区(例如 可变长列表布局 的偏移量)是合理的。曾考虑过这种布局,但最终决定使用子数组。
子数组允许我们保持与父数组关联的“逻辑长度”(解码后的长度)和与子数组关联的“物理长度”(运行结束的数量)。如果 run_ends
是父数组中的一个缓冲区,那么缓冲区的大小将与数组的长度无关,这将令人困惑。
一个运行必须至少有长度 1。这意味着运行结束数组中的值都为正且严格递增。运行结束不能为 null。
REE 父数组没有有效性位图,其空值计数字段应始终为 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 |
可变参数缓冲区 |
---|---|---|---|---|
原始类型 |
有效性 |
数据 |
||
可变长二进制 |
有效性 |
偏移量 |
数据 |
|
可变长二进制视图 |
有效性 |
视图 |
数据 |
|
List |
有效性 |
偏移量 |
||
List View |
有效性 |
偏移量 |
大小 |
|
Fixed-size List |
有效性 |
|||
Struct |
有效性 |
|||
稀疏联合 |
类型 ID (type ids) |
|||
稠密联合 |
类型 ID (type ids) |
偏移量 |
||
Null |
||||
字典编码 |
有效性 |
数据 (索引) |
||
运行结束编码 |
序列化和进程间通信 (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 值,该值内部包含:
版本号
特定消息值(
Schema
,RecordBatch
, 或DictionaryBatch
中的一个)消息体大小
用于存放应用程序提供的任何元数据的
custom_metadata
字段
从输入流读取时,通常首先解析和验证 Message
元数据以获取消息体大小。然后就可以读取消息体。
模式消息#
Flatbuffers 文件 Schema.fbs 包含所有内建数据类型和表示给定记录批次模式的 Schema
元数据类型的定义。模式由有序的字段序列组成,每个字段都有名称和类型。序列化的 Schema
不包含任何数据缓冲区,只包含类型元数据。
Field
Flatbuffers 类型包含单个数组的元数据。这包括:
字段名称
字段数据类型
字段在语义上是否可空。虽然这对数组的物理布局没有影响,但许多系统区分可空和不可空字段,我们希望允许它们保留此元数据以实现模式的忠实往返。
子
Field
值的集合,用于嵌套类型一个
dictionary
属性,指示字段是否进行字典编码。如果是,将分配一个字典“id”,以便将后续字典 IPC 消息与相应的字段匹配。
我们还提供模式级别和字段级别的 custom_metadata
属性,允许系统插入自己的应用程序定义元数据来自定义行为。
记录批次消息#
记录批次消息包含与模式决定的物理内存布局对应的实际数据缓冲区。此消息的元数据提供了每个缓冲区的位置和大小,允许使用指针算术重建 Array 数据结构,从而无需内存拷贝。
记录批次的序列化形式如下:
数据头,定义为 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 值描述了一块内存的位置和大小。通常,它们相对于下方定义的 封装消息格式 来解释。
Buffer
的 size
字段不需要考虑填充字节。由于此元数据可用于在库之间通信内存中的指针地址,建议将 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
压缩编解码器压缩。消息体的展平序列中的缓冲区必须使用相同的编解码器单独压缩。压缩缓冲区序列中的特定缓冲区可以保持未压缩(例如,如果压缩这些特定缓冲区不会显著减小其大小)。
使用的压缩类型在 记录批次消息 的 data header
中定义,位于可选的 compression
字段中,默认值为未压缩。
注意
lz4
压缩编解码器表示 LZ4 帧格式,不应与 “原始”(也称为“块”)格式 混淆。
序列化形式中压缩缓冲区和未压缩缓冲区之间的区别如下:
如果 记录批次消息 中的缓冲区是 压缩的:
data header
包括记录批次消息体中每个 压缩缓冲区 的长度和内存偏移量,以及压缩类型body
包括一个 压缩缓冲区 的展平序列,以及 未压缩缓冲区的长度,作为存储在该序列中每个缓冲区的前 8 字节中的一个 64 位小端序有符号整数。该未压缩长度可以设置为-1
,以指示该特定缓冲区保持未压缩。
如果 记录批次消息 中的缓冲区是 未压缩的:
data header
包括记录批次消息体中每个 未压缩缓冲区 的长度和内存偏移量body
包括一个 未压缩缓冲区 的展平序列。
注意
一些 Arrow 实现缺乏对使用上述任一编解码器生成和消费带有压缩缓冲区的 IPC 数据的支持。详见 实现状态。
一些应用程序可能在其用于存储或传输 Arrow IPC 数据的协议中应用压缩。(例如,HTTP 服务器可能提供 gzip 压缩的 Arrow IPC 流。)已经在其存储或传输协议中使用压缩的应用程序应避免使用缓冲区压缩。双重压缩通常会降低性能,并且不能显著提高压缩率。
字节序 (端序)#
Arrow 格式默认为小端序。
序列化的 Schema 元数据有一个 endianness 字段,指示 RecordBatch 的端序。通常,这是生成 RecordBatch 的系统的端序。主要用例是在具有相同端序的系统之间交换 RecordBatch。首先,当尝试读取与底层系统端序不匹配的 Schema 时,我们将返回一个错误。参考实现专注于小端序,并为其提供测试。最终,我们可能会提供通过字节交换的自动转换。
IPC 流式格式#
我们为记录批次提供了一种流式协议或“格式”。它以一系列封装消息的形式呈现,每条消息都遵循上述格式。模式首先出现在流中,并且对所有后续记录批次都是相同的。如果模式中的任何字段是字典编码的,则会包含一个或多个 DictionaryBatch
消息。 DictionaryBatch
和 RecordBatch
消息可以交错,但在 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 字节(包含 4 字节继续指示器 (0xFFFFFFFF
) 后跟 0 元数据长度 (0x00000000
))或关闭流接口来发出流结束(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 协议消息中传递应用程序特定元数据。这包括 Field
、Schema
和 Message
。
冒号符号 :
用作命名空间分隔符。它可以在键中多次使用。
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 规范的子集和/或对其进行扩展,但需满足以下约束:
实现规范的子集#
如果仅生成(不消费)Arrow 向量:可以实现向量规范及其相应元数据的任何子集。
如果消费和生成向量:需要支持向量的最小子集。生成向量的子集及其相应元数据总是可以的。向量的消费应至少将不支持的输入向量转换为支持的子集(例如,将 Timestamp.millis 转换为 timestamp.micros 或将 int32 转换为 int64)。
可扩展性#
执行引擎实现者也可以在内部使用自己的向量扩展其内存表示,只要这些向量不被暴露。在将数据发送到期望接收 Arrow 数据的其他系统之前,这些自定义向量应转换为 Arrow 规范中存在的类型。