一个 Variant 表示以下三种类型之一:
- 原始类型(Primitive):一种类型及其对应的值,例如
INT、STRING
- 数组(Array):由 Variant 值组成的有序列表
- 对象(Object):由字符串和 Variant 组成的无序键值对集合。一个对象不能包含重复的键
一个 Variant 使用两个二进制值进行编码:值和元数据。
允许使用的原始类型数量是固定的,具体见下表。这些类型是 Parquet 格式所支持的逻辑类型中被普遍支持的一个子集。
Variant 二进制编码可以将半结构化数据(例如 JSON)表示为一种能够按路径进行高效查询的形式。即使数据结构非常宽或嵌套非常深,该设计也能够高效访问嵌套数据。
采用这种表示方式的另一个原因是:除元数据外,每个嵌套的 Variant 值都是连续存储且自包含的。
例如,当一个 Variant 包含由多个 Variant 值组成的数组时,只要将其中某个内部 Variant 值与完整 Variant 的元数据配对,该内部值本身就是一个合法的 Variant。
本文档描述 Variant 二进制编码方案。
Variant 字段还可以进行“拆分”(shredding)。拆分是指把 Variant 中的某些元素提取到单独的列中,以实现更高效的数据提取和过滤下推。[Variant 拆分规范](VariantShredding.md)描述了如何将 Variant 值拆分成带类型的 Parquet 列。
Parquet 中的 Variant
在 Parquet 中,一个 Variant 值由一个包含两个字段的 Group 表示,这两个字段分别名为
value 和 metadata。- Variant Group 必须使用
VARIANT逻辑类型进行标注。
value和metadata字段都必须是binary类型,即 Parquet Thrift 定义中的BYTE_ARRAY。
metadata字段必须标记为required,并且必须是符合下文定义的有效 Variant 元数据。
- 对于未拆分的 Variant 值,
value字段必须标记为required;如果值的一部分已经被[拆分](VariantShredding.md)为带类型的 Parquet 列,则应标记为optional。
- 如果
value字段存在,它必须是符合下文定义的有效 Variant 值。
以下是 Parquet 中未拆分 Variant 的预期表示形式:
以下是 Parquet 中已拆分 Variant 的一个示例:
VARIANT 注解本身不会对 Variant Group 的重复性施加额外限制,但包含它的类型(例如 MAP 和 LIST)可能对此作出限制。Variant Group 的名称就是 Variant 列的名称。
元数据编码
编码后的元数据始终以一个头字节开始:
version 是一个 4 位值,当前必须始终为 1。sorted_strings 是一个 1 位值,表示字典中的字符串是否经过排序且互不重复。offset_size_minus_one 是一个 2 位值,用于表示每个字典大小字段和偏移量字段所占的字节数。实际字节数 offset_size 的计算方式为:第 5 位(标记为
R)是保留位,读取器必须忽略它。完整元数据的编码结构如下:
元数据首先编码
header 字节,然后编码 dictionary_size。dictionary_size 是一个长度为 offset_size 字节的无符号小端序整数,表示字典中的字符串数量。接下来是
offset 列表,其中包含 dictionary_size + 1 个值。每个 offset 都是一个长度为 offset_size 字节的无符号小端序整数,表示第 i 个字符串在 bytes 中的起始字节偏移量。第一个
offset 始终为 0,最后一个 offset 始终等于 bytes 的总长度。元数据的最后一部分是
bytes,用于保存字典中的所有字符串。所有字符串都必须使用 UTF-8 编码。元数据编码语法
编码后元数据的语法如下:
对应说明如下:
metadata:由<header>、<dictionary_size>和<dictionary>组成。
header:1 个字节,编码方式为:
version:4 位版本 ID,当前必须始终为1。
sorted_strings:1 位值,表示字典中的字符串是否已排序且互不重复。
offset_size_minus_one:2 位值,表示字典大小字段和偏移量字段各自使用多少字节。
dictionary_size:长度为offset_size字节的无符号小端序整数,表示字典中的字符串数量。
dictionary:由若干<offset>和<bytes>组成。
offset:长度为offset_size字节的无符号小端序整数,表示第 i 个字符串在bytes中的起始位置。列表中应包含dictionary_size + 1个值,最后一个值是bytes的总长度。
bytes:采用 UTF-8 编码的字典字符串值。
注意事项:
- 所有偏移量都相对于
bytes数组的起始位置。
- 第 i 个字符串的长度可以按以下方式计算:
- 第一个字符串的偏移量始终为
0,因此从信息角度看它是冗余的。规范仍然保留该值,以简化内存中的处理。
offset_size_minus_one表示每个dictionary_size和offset条目使用的字节数:- 值为
0:1 字节 - 值为
1:2 字节 - 值为
2:3 字节 - 值为
3:4 字节
- 如果
sorted_strings为1,字典中的字符串必须互不重复,并按字典序排序。
- 如果
sorted_strings为0,读取器不能对字符串的顺序或唯一性作任何假设。
值编码
完整的 Variant 值编码首先包含
value_metadata 字节,然后包含零个或多个用于表示 val 的字节:基本类型
basic_type 是一个 2 位值,表示 Variant 值属于哪一种基本类型。每个值的含义见基本类型表。值头部
value_header 是一个 6 位值,用于保存更多类型信息。其格式取决于 basic_type。原始类型的值头部(basic_type=0)
当
basic_type 为 0 时,value_header 是一个 6 位的 primitive_header。每个值的含义见原始类型表。
短字符串的值头部(basic_type=1)
当
basic_type 为 1 时,value_header 是一个 6 位的 short_string_header:short_string_header 的值就是字符串的长度。对象的值头部(basic_type=2)
当
basic_type 为 2 时,value_header 由以下字段组成:field_offset_size_minus_one
field_id_size_minus_one
is_large
field_offset_size_minus_one 和 field_id_size_minus_one 都是 2 位值,分别表示字段偏移量和字段 ID 使用多少字节进行编码。实际字节数的计算方式如下:
is_large 是一个 1 位值,表示使用多少字节来编码元素数量:is_large=0:使用 1 个字节
is_large=1:使用 4 个字节
第 5 位(标记为
R)是保留位,读取器必须忽略它。数组的值头部(basic_type=3)
当
basic_type 为 3 时,value_header 由以下字段组成:field_offset_size_minus_one
is_large
field_offset_size_minus_one 是一个 2 位值,表示用于编码字段偏移量的字节数。实际字节数为:
is_large 是一个 1 位值,表示元素数量字段使用多少字节:is_large=0:使用 1 个字节
is_large=1:使用 4 个字节
第 5~3 位(标记为
RRR)是保留位,读取器必须忽略它们。值数据
value_data 的编码格式取决于 value_metadata 指定的类型。对于某些类型,value_data 的长度为零字节。原始类型的值数据(basic_type=0)
当
basic_type 为 0 时,value_data 的格式取决于 primitive_header。每种原始类型的具体编码格式见原始类型表。
短字符串的值数据(basic_type=1)
当
basic_type 为 1 时,value_data 是表示该字符串的一系列 UTF-8 编码字节。对象的值数据(basic_type=2)
当
basic_type 为 2 时,value_data 用于编码对象。其编码格式如下:对象的
value_data 首先是 num_elements。它是一个长度为 1 字节或 4 字节的无符号小端序整数,表示对象中的元素数量。num_elements 的字节数由 value_header 中的 is_large 指定。接下来是
field_id 列表。该列表包含 num_elements 个条目,每个 field_id 都是一个长度为 field_id_size 字节的无符号小端序整数。field_id 是元数据字典的索引。field_id 列表之后是 field_offset 列表。该列表包含 num_elements + 1 个条目,每个 field_offset 都是一个长度为 field_offset_size 字节的无符号小端序整数。field_offset 表示第 i 个 value 开始位置的字节偏移量。该偏移量相对于第一个 value 的第一个字节。最后一个
field_offset 指向最后一个 value 结束位置之后的第一个字节。field_offset 列表之后是 value 列表。它包含 num_elements 个 value 条目,每个条目都是一个编码后的 Variant 值。对于对象中的第 i 个键值对:
- 键是由第 i 个
field_id索引的元数据字典条目;
- 值是从第 i 个
field_offset所指向的字节位置开始的 Variantvalue。
字段 ID 和字段偏移量必须按照元数据字典中对应字段名称的字典序排列。
不过,实际的
value 条目不必按照任何特定顺序存放。这意味着 field_offset 的值不一定单调递增。例如,对于以下对象:
field_id 列表必须按照字段名称的字典序排列:field_offset 列表必须是:但
value 列表本身可以采用任意顺序。数组的值数据(basic_type=3)
当
basic_type 为 3 时,value_data 用于编码数组。其格式如下:数组的
value_data 首先是 num_elements。它是一个长度为 1 字节或 4 字节的无符号小端序整数,表示数组中的元素数量。num_elements 的字节数由 value_header 中的 is_large 指定。接下来是
field_offset 列表。该列表包含 num_elements + 1 个条目,每个 field_offset 都是一个长度为 field_offset_size 字节的无符号小端序整数。field_offset 表示第 i 个 value 的起始字节偏移量。该偏移量相对于第一个 value 的第一个字节。最后一个
field_offset 指向最后一个 value 的最后一个字节之后的位置。field_offset 列表之后是 value 列表。它包含 num_elements 个 value 条目,每个条目都是一个编码后的 Variant 值。对于数组中的第 i 个条目,其值是从第 i 个
field_offset 所指向位置开始的 Variant value。值编码语法
编码后值的语法如下:
对应说明如下:
value:由<value_metadata>和一个可选的<value_data>组成。
value_metadata:1 个字节,编码方式为:
basic_type:基本类型表中的 ID。<value_header>必须采用与该基本类型对应的形式。
value_header:以下类型之一:
primitive_header:原始类型表中的 ID。<val>必须是对应的<primitive_val>形式。
short_string_header:以字节为单位的无符号字符串长度,范围为 0~63。
object_header:
array_header:
value_data:原始值、短字符串值、对象值或数组值之一。
primitive_val:二进制表示形式见类型表。
short_string_val:UTF-8 编码的字节。
object_val:
array_val:
num_elements:长度为 1 字节或 4 字节的无符号小端序整数,具体取决于<object_header>或<array_header>中的is_large。
field_id:长度为 1、2、3 或 4 字节的无符号小端序整数。其长度由<object_header>中的field_id_size_minus_one决定,用于索引字典。
field_offset:长度为 1、2、3 或 4 字节的无符号小端序整数。其长度由<object_header>或<array_header>中的field_offset_size_minus_one决定,表示在fields内的字节偏移量。
fields:零个或多个<value>。
每个
value_data 都必须与 value_metadata 定义的类型相对应。布尔类型和空值类型没有相应的
value_data,因为它们的类型本身已经定义了对应的值。每个
array_val 和 object_val 必须为 field_offset 包含恰好 num_elements + 1 个值。最后一个条目表示最后一个字段之后一个字节的位置,也就是所有字段的总字节数。所有偏移量都相对于对象或数组中第一个字段的第一个字节。
field_id_size_minus_one 和 field_offset_size_minus_one 表示每个字段 ID 或偏移量使用的字节数:- 值为
0:1 字节
- 值为
1:2 字节
- 值为
2:3 字节
- 值为
3:4 字节
数组和对象的
is_large 标志用于表示元素数量使用 1 字节还是 4 字节编码。当元素数量超过 255 时,
is_large 必须设置为 true。实现也可以为这些字段使用大于实际所需的编码宽度。例如,一个少于 256 个元素的对象也可以将
is_large 设置为 true。“短字符串”基本类型是一种优化方式。对于长度小于 64 字节的字符串,可以直接把字符串长度编码进类型字节中。它在语义上与原始类型中的“字符串”完全相同。
Decimal 类型包含 Scale,但不包含 Precision。十进制值的隐含精度为:
当
val 为 0 时,精度为 1。注意:Variant 二进制编码中的 Decimal 值使用小端序保存未缩放值。这与 Parquet 的
DECIMAL 逻辑类型不同:后者对于 BYTE_ARRAY 和 FIXED_LEN_BYTE_ARRAY 物理类型使用大端序补码编码。编码类型
Variant 基本类型
基本类型 | ID | 说明 |
原始类型(Primitive) | 0 | 原始类型之一 |
短字符串(Short string) | 1 | 长度小于 64 字节的字符串 |
对象(Object) | 2 | 由“字符串键、Variant 值”键值对组成的集合 |
数组(Array) | 3 | Variant 值组成的有序序列 |
Variant 原始类型
等价类别 | Variant 物理类型 | 类型 ID | 等价的 Parquet 类型 | 二进制格式 |
NullType | null | 0 | UNKNOWN | 无 |
Boolean | boolean (True) | 1 | BOOLEAN | 无 |
Boolean | boolean (False) | 2 | BOOLEAN | 无 |
Exact Numeric | int8 | 3 | INT(8, true) | 1 字节 |
Exact Numeric | int16 | 4 | INT(16, true) | 2 字节,小端序 |
Exact Numeric | int32 | 5 | INT(32, true) | 4 字节,小端序 |
Exact Numeric | int64 | 6 | INT(64, true) | 8 字节,小端序 |
Double | double | 7 | DOUBLE | IEEE 小端序 |
Exact Numeric | decimal4 | 8 | DECIMAL(precision, scale) | 1 字节 Scale,范围为 [0, 38];随后是小端序的未缩放值,参见 Decimal 表 |
Exact Numeric | decimal8 | 9 | DECIMAL(precision, scale) | 1 字节 Scale,范围为 [0, 38];随后是小端序的未缩放值,参见 Decimal 表 |
Exact Numeric | decimal16 | 10 | DECIMAL(precision, scale) | 1 字节 Scale,范围为 [0, 38];随后是小端序的未缩放值,参见 Decimal 表 |
Date | date | 11 | DATE | 4 字节,小端序 |
Timestamp | timestamp | 12 | TIMESTAMP(isAdjustedToUTC=true, MICROS) | 8 字节,小端序 |
TimestampNTZ | timestamp without time zone | 13 | TIMESTAMP(isAdjustedToUTC=false, MICROS) | 8 字节,小端序 |
Float | float | 14 | FLOAT | IEEE 小端序 |
Binary | binary | 15 | BYTE_ARRAY | 4 字节小端序长度,随后是数据字节 |
String | string | 16 | STRING | 4 字节小端序长度,随后是 UTF-8 编码字节 |
TimeNTZ | time without time zone | 17 | TIME(isAdjustedToUTC=false, MICROS) | 8 字节,小端序 |
Timestamp | timestamp with time zone | 18 | TIMESTAMP(isAdjustedToUTC=true, NANOS) | 8 字节,小端序 |
TimestampNTZ | timestamp without time zone | 19 | TIMESTAMP(isAdjustedToUTC=false, NANOS) | 8 字节,小端序 |
UUID | uuid | 20 | UUID | 16 字节,大端序 |
“等价类别”列表示采用不同物理编码的类型在逻辑上是等价的。
例如,一个用户表达式在处理内容为
"hello" 的字符串时,无论该字符串使用短字符串优化,还是使用长字符串编码,都应该表现出相同的行为。同样,一个用户表达式在处理值为
1 的 int8 时,其行为应该与处理 Scale 为 2、未缩放值为 100 的 decimal16 相同。Decimal 表
Decimal 精度 | Decimal 值类型 | Variant 物理类型 |
1 <= precision <= 9 | int32 | decimal4 |
10 <= precision <= 18 | int64 | decimal8 |
19 <= precision <= 38 | int128 | decimal16 |
> 38 | 不支持 | ㅤ |
字符串值必须使用 UTF-8 编码
Variant 二进制格式中的所有字符串都必须使用 UTF-8 编码,包括:
- 字典中的键字符串;
- “短字符串”值;
- “长字符串”值。
对象字段 ID 的顺序和唯一性
对于对象,字段 ID 和偏移量必须按照对应字段名称的字典序排列。排序时,对 UTF-8 字节使用无符号字节顺序。
需要注意的是,字段值本身不必遵循该顺序。因此,偏移量不一定按照升序排列。
为了让 Variant 值的构造更加灵活,字段值不必与字段 ID 使用相同的顺序。
实现可以依赖字段 ID 的这种顺序来搜索字段名称。例如,可以结合元数据查询,对字段 ID 执行二分查找,从而找到指定名称的字段。
字段名称区分大小写。
每个对象中的字段名称必须唯一。无论两个同名字段是否使用不同的字典 ID,对象中包含两个相同名称的字段都属于错误。
版本与扩展
实现不需要解析元数据版本高于其所支持版本的 Variant 值。
不过,规范可以在不递增版本 ID 的情况下添加新类型。出现这种情况时,如果实现希望继续读取,它应当能够读取 Variant 值的其余部分。
拆分
当只需要读取少数字段时,把整个 Variant 对象保存在单列中可能导致较差的读取性能。
更好的方法是为各个字段创建独立的列。这种方式称为拆分(shredding)或子列化(subcolumnarization)。
[VariantShredding.md](VariantShredding.md) 描述了 Parquet 中的 Variant 拆分规范。