一个 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 所指向的字节位置开始的 Variant value。
      字段 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 拆分规范。