DeepSeek总结的parquet Variant “碎形化“技术

来源:https://github.com/apache/parquet-format/blob/master/VariantShredding.md

Variant 碎形化 (Shredding)

Variant 类型旨在高效存储和处理半结构化数据,即使面对异构值也是如此。查询引擎将每个 Variant 值以一种自描述格式编码,并将其作为包含 valuemetadata 二进制字段的 group 存储在 Parquet 中。由于数据通常是部分同构的,将某些字段提取到单独的 Parquet 列中以进一步提高性能是有益的。这个过程称为碎形化 (shredding)

碎形化使得能够利用 Parquet 的列式表示来实现更紧凑的数据编码、用于数据跳过的列统计信息以及部分投影。

例如,查询 SELECT variant_get(event, '$.event_ts', 'timestamp') FROM tbl 只需要加载 event_ts 字段,如果该列已被碎形化,则可以通过列式投影读取它,而无需读取或反序列化 event Variant 的其余部分。类似地,对于查询 SELECT * FROM tbl WHERE variant_get(event, '$.event_type', 'string') = 'signup'event_type 碎形化列的元数据可用于跳过数据并延迟加载 Variant 的其余部分。

Variant 元数据

无论 Variant 值是否被碎形化,Variant 元数据都存储在顶层 Variant group 的一个二进制 metadata 列中。

Variant 中的所有 value 列必须使用相同的 metadata。Variant 的所有字段名,无论是否被碎形化,都必须存在于元数据中。

值碎形化

Variant 值存储在名为 value 的 Parquet 字段中。每个 value 字段可能有一个关联的碎形化字段,名为 typed_value,当值与特定类型匹配时,该字段存储该值。当存在 typed_value 时,读取器必须根据此规范重构碎形化值。

例如,一个 Variant 字段 measurement 可以通过添加类型为 int64typed_value 作为长整型值进行碎形化:

复制代码
required group measurement (VARIANT(1)) {
  required binary metadata;
  optional binary value;
  optional int64 typed_value;
}

用于存储 variant 元数据和值的 Parquet 列必须按名称访问,而不是按位置。

一系列测量值 34, null, "n/a", 100 将存储为:

metadata value typed_value
34 01 00 v1/空 null 34
null 01 00 v1/空 00 (null) null
"n/a" 01 00 v1/空 13 6E 2F 61 (n/a) null
100 01 00 v1/空 null 100

valuetyped_value 都是用于编码单个值的可选字段。这两个字段中的值必须根据下表进行解释:

value typed_value 含义
null null 值缺失;仅对碎形化对象字段有效
非 null null 值存在,可以是任何类型,包括 null
null 非 null 值存在,并且是碎形化类型
非 null 非 null 值存在,并且是部分碎形化的对象

value 是一个对象且 typed_value 是一个碎形化对象时,该对象被称为部分碎形化 。写入器不得生成 valuetyped_value 都非 null 的数据,除非 Variant 值是一个对象。

如果在需要值的上下文中缺少 Variant,读取器必须返回一个 Variant null (00):基本类型 0 (primitive) 和物理类型 0 (null)。例如,如果需要一个 Variant(如上面的 measurement),并且 valuetyped_value 都为 null,则返回的 value 必须是 00 (Variant null)。

碎形化值类型

碎形化值必须使用以下 Parquet 类型:

Variant 类型 Parquet 物理类型 Parquet 逻辑类型
boolean BOOLEAN
int8 INT32 INT(8, signed=true)
int16 INT32 INT(16, signed=true)
int32 INT32
int64 INT64
float FLOAT
double DOUBLE
decimal4 INT32 DECIMAL(P, S)
decimal8 INT64 DECIMAL(P, S)
decimal16 BYTE_ARRAY / FIXED_LEN_BYTE_ARRAY DECIMAL(P, S)
date INT32 DATE
time INT64 TIME(false, MICROS)
timestamptz(6) INT64 TIMESTAMP(true, MICROS)
timestamptz(9) INT64 TIMESTAMP(true, NANOS)
timestampntz(6) INT64 TIMESTAMP(false, MICROS)
timestampntz(9) INT64 TIMESTAMP(false, NANOS)
binary BINARY
string BINARY STRING
uuid FIXED_LEN_BYTE_ARRAYlen=16 UUID
array GROUP; 见下面的数组 LIST
object GROUP; 见下面的对象
基本类型

基本类型值可以使用上表中等效的 Parquet 基本类型作为 typed_value 进行碎形化。

除非该值作为对象被碎形化(参见对象),否则 typed_valuevalue(但不能同时)必须非 null。

数组

数组可以通过为 typed_value 使用 3 级 Parquet 列表进行碎形化。

如果该值不是数组,则 typed_value 必须为 null。如果该值是数组,则 value 必须为 null。

列表 element 必须是一个 required group。element group 可以包含 valuetyped_value 字段。当 typed_value 不存在或无法表示元素时,元素的 value 字段将元素存储为 Variant 编码的 binary。当不将元素碎形化为特定类型时,可以省略 typed_value 字段。当将元素碎形化为特定类型时,可以省略 value 字段。但是,这两个字段中至少必须存在一个。

例如,一个 tags Variant 可以使用以下定义碎形化为一个字符串列表:

复制代码
optional group tags (VARIANT(1)) {
  required binary metadata;
  optional binary value;
  optional group typed_value (LIST) {   # 必须为 optional 以允许 null 列表
    repeated group list {
      required group element {          # 碎形化元素
        optional binary value;
        optional binary typed_value (STRING);
      }
    }
  }
}

数组的所有元素都必须存在(不能缺失),因为数组 Variant 编码不允许缺失元素。也就是说,typed_valuevalue(但不能同时)必须非 null。null 元素必须在 value 中编码为 Variant null:基本类型 0 (primitive) 和物理类型 0 (null)。

一系列 tags 数组 ["comedy", "drama"], ["horror", null], ["comedy", "drama", "romance"], null 将存储为:

数组 value typed_value typed_value...value typed_value...typed_value
["comedy", "drama"] null 非 null null, null `comedy`, `drama`
["horror", null] null 非 null null, `00` `horror`, null
["comedy", "drama", "romance"] null 非 null null, null, null `comedy`, `drama`, `romance`
null 00 (null) null
对象

对象的字段可以使用一个包含碎形化字段的 Parquet group 作为 typed_value 进行碎形化。

如果该值是对象,则 typed_value 必须非 null。如果该值不是对象,则 typed_value 必须为 null。如果 typed_value 为 null,读取器可以假定该值不是对象,并且 typed_value 字段值是正确的;也就是说,如果 typed_value 字段满足所需字段,读取器不需要读取 value 列。

typed_value group 中的每个碎形化字段都表示为一个 required group,其中包含可选的 valuetyped_value 字段。当 typed_value 无法表示该字段时,value 字段将该值存储为 Variant 编码的 binary。这种布局使读取器能够基于 valuetyped_value 的字段统计信息跳过数据。当不将字段碎形化为特定类型时,可以省略 typed_value 字段。

部分碎形化对象的 value 列绝不能包含由 typed_value 中的 Parquet 列表示的字段(碎形化字段)。读取器可以始终假定数据被正确写入,并且 typed_value 中的碎形化字段不会出现在 value 中。因此,当一个字段同时定义在 value 和碎形化字段 typed_value 中时,读取结果可能不一致。

例如,一个 Variant event 字段可以使用以下定义碎形化 event_type (string) 和 event_ts (timestamp) 列:

复制代码
optional group event (VARIANT(1)) {
  required binary metadata;
  optional binary value;                # 一个 variant,预期是一个对象
  optional group typed_value {          # variant 对象的碎形化字段
    required group event_type {         # event_type 的碎形化字段
      optional binary value;
      optional binary typed_value (STRING);
    }
    required group event_ts {           # event_ts 的碎形化字段
      optional binary value;
      optional int64 typed_value (TIMESTAMP(true, MICROS));
    }
  }
}

每个命名字段的 group 必须使用重复级别 required

字段的 valuetyped_value 被设置为 null(缺失),以指示该字段在 variant 中不存在。要编码一个存在但值为 null 的字段,value 必须包含一个 Variant null:基本类型 0 (primitive) 和物理类型 0 (null)。

当一个字段的 valuetyped_value 都非 null 时,引擎应该失败。如果引擎选择在这种情况下读取,则必须使用 typed_value 列。读取器可以始终假定数据被正确写入,并且只定义了 valuetyped_value 中的一个。因此,当 valuetyped_value 都定义时,读取结果可能与只需要其中一列的优化读取不一致。

下表显示了第一列中的一系列对象将如何存储:

Event 对象 value typed_value typed_value.event_type.value typed_value.event_type.typed_value typed_value.event_ts.value typed_value.event_ts.typed_value 注释
{"event_type": "noop", "event_ts": 1729794114937} null 非 null null noop null 1729794114937 完全碎形化对象
{"event_type": "login", "event_ts": 1729794146402, "email": "user@example.com"} {"email": "user@example.com"} 非 null null login null 1729794146402 部分碎形化对象
{"error_msg": "malformed: ..."} {"error_msg": "malformed: ..."} 非 null null null null null 所有碎形化字段都缺失的对象
"malformed: not an object" malformed: not an object null 不是对象(存储为 Variant 字符串)
{"event_ts": 1729794240241, "click": "_button"} {"click": "_button"} 非 null null null null 1729794240241 字段 event_type 缺失
{"event_type": null, "event_ts": 1729794954163} null 非 null 00 (字段存在,且为 null) null null 1729794954163 字段 event_type 存在且为 null
{"event_type": "noop", "event_ts": "2024-10-24"} null 非 null null noop "2024-10-24" null 字段 event_ts 存在但不是时间戳
{ } null 非 null null null null null 对象存在但为空
null 00 (null) null 对象/值为 null
缺失 null null 对象/值缺失
无效: {"event_type": "login", "event_ts": 1729795057774} {"event_type": "login"} 非 null null login null 1729795057774 无效: 碎形化字段存在于 value
无效: {"event_type": "login"} {"event_type": "login"} null 无效: 碎形化字段存在于 value 中,而 typed_value 为 null
无效: "a" "a" 非 null null null null null 无效: typed_value 存在且 value 不是对象
无效: {} 02 00 (包含 0 个字段的对象) null 无效: 对象的 typed_value 为 null

上表中的无效情况不得由写入器产生。当 typed_value 非 null 且包含碎形化字段时,读取器必须返回一个对象。

嵌套

与任何 Variant value 字段关联的 typed_value 可以是任何碎形化类型,如上文各节所示。

例如,上面的 event 对象也可以将子字段碎形化为对象 (location) 或数组 (tags)。

复制代码
optional group event (VARIANT(1)) {
  required binary metadata;
  optional binary value;
  optional group typed_value {
    required group event_type {
      optional binary value;
      optional binary typed_value (STRING);
    }
    required group event_ts {
      optional binary value;
      optional int64 typed_value (TIMESTAMP(true, MICROS));
    }
    required group location {
      optional binary value;
      optional group typed_value {
        required group latitude {
          optional binary value;
          optional double typed_value;
        }
        required group longitude {
          optional binary value;
          optional double typed_value;
        }
      }
    }
    required group tags {
      optional binary value;
      optional group typed_value (LIST) {
        repeated group list {
          required group element {
            optional binary value;
            optional binary typed_value (STRING);
          }
        }
      }
    }
  }
}

数据跳过

value 始终为 null(缺失)时,typed_value 列的统计信息可用于文件、行组或页面跳过。

当相应的 value 列全为 null 时,所有值必须是碎形化 typed_value 字段的类型。由于类型已知,与该类型值的比较是有效的。IS NULL/IS NOT NULLIS NAN/IS NOT NAN 的过滤结果也是有效的。

与其他类型值的比较不一定有效,不应跳过数据。

Variant 的类型转换行为委托给处理引擎。例如,将字符串解释为时间戳可能取决于引擎的 SQL 会话时区。

重构碎形化 Variant

可以使用递归算法恢复未碎形化的 Variant 值,其中初始调用使用顶层 Variant group 字段调用 construct_variant

python 复制代码
def construct_variant(metadata: Metadata, value: Variant, typed_value: Any) -> Variant:
    """从 value 和 typed_value 构造 Variant"""
    if typed_value is not None:
        if isinstance(typed_value, dict):
            # 这是一个碎形化对象
            object_fields = {
                name: construct_variant(metadata, field.value, field.typed_value)
                for (name, field) in typed_value
            }

            if value is not None:
                # 这是一个部分碎形化对象
                assert isinstance(value, VariantObject), "部分碎形化的值必须是一个对象"
                assert typed_value.keys().isdisjoint(value.keys()), "对象键必须不重叠"

                # 合并碎形化字段和非碎形化字段
                # (字段 ID 和偏移量必须按对应字段名的顺序排列,
                # 按字典顺序排序(UTF-8 的无符号字节顺序))
                return VariantObject(metadata, object_fields).union(VariantObject(metadata, value))

            else:
                return VariantObject(metadata, object_fields)

        elif isinstance(typed_value, list):
            # 这是一个碎形化数组
            assert value is None, "碎形化数组不得与 variant 值冲突"

            elements = [
                construct_variant(metadata, elem.value, elem.typed_value)
                for elem in list(typed_value)
            ]
            return VariantArray(metadata, elements)

        else:
            # 这是一个碎形化基本类型
            assert value is None, "碎形化基本类型不得与 variant 值冲突"

            return primitive_to_variant(typed_value)

    elif value is not None:
        return Variant(metadata, value)

    else:
        # value 缺失
        return None

def primitive_to_variant(typed_value: Any): Variant:
    if isinstance(typed_value, int):
        return VariantInteger(typed_value)
    elif isinstance(typed_value, str):
        return VariantString(typed_value)
    ...

向后和向前兼容性

碎形化是 Variant 的一个可选特性,读取器必须能够继续读取仅包含 valuemetadata 字段的 group。

不写入碎形化值的引擎必须能够根据此规范读取碎形化值,或者必须失败。

不同的文件可能包含冲突的碎形化 schema。也就是说,文件可能为同一个 Variant 包含具有不兼容类型的不同 typed_value 列。可能无法推断或指定一个单一的碎形化 schema,使得无需将值重构为 Variant 即可读取表的所有 Parquet 文件。

相关推荐
云计算磊哥@1 小时前
运维开发宝典030-MySQL06数据库运维阶段总结
运维·数据库·运维开发
这个DBA有点耶1 小时前
国产数据库有哪些?2026年主流产品选型对比
数据库·程序人生·职场和发展·架构·程序员创富·改行学it
pFg0v4O7P1 小时前
从Cursor迁移到Claude Code:完整过渡指南
数据库
W001hhh2 小时前
260615PM
数据库
吴声子夜歌2 小时前
SQL经典实例——元数据查询
数据库·sql
睡不醒男孩0308232 小时前
生产环境故障销账:PostgreSQL 突发连接数暴涨与死锁,如何利用 CLup 秒级定位与解锁?
运维·数据库
2601_962054952 小时前
终端与IDE形态的vibe coding实测:两款AI编程工具迭代能力对比
数据库·ide·ai编程
万岳科技2 小时前
教育培训系统开发流程详解:平台建设关键环节解析
数据库·后端·学习
Nturmoils2 小时前
线上修一批脏数据,先别急着全量重来
数据库·后端