DeepSeek总结的parquet Variant “碎形化“技术
来源:https://github.com/apache/parquet-format/blob/master/VariantShredding.md
Variant 碎形化 (Shredding)
Variant 类型旨在高效存储和处理半结构化数据,即使面对异构值也是如此。查询引擎将每个 Variant 值以一种自描述格式编码,并将其作为包含value和metadata二进制字段的 group 存储在 Parquet 中。由于数据通常是部分同构的,将某些字段提取到单独的 Parquet 列中以进一步提高性能是有益的。这个过程称为碎形化 (shredding)。
碎形化使得能够利用 Parquet 的列式表示来实现更紧凑的数据编码、用于数据跳过的列统计信息以及部分投影。
例如,查询SELECT variant_get(event, '$.event_ts', 'timestamp') FROM tbl只需要加载event_ts字段,如果该列已被碎形化,则可以通过列式投影读取它,而无需读取或反序列化eventVariant 的其余部分。类似地,对于查询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可以通过添加类型为int64的typed_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 00v1/空 | null | 34 |
| null | 01 00v1/空 | 00(null) | null |
| “n/a” | 01 00v1/空 | 13 6E 2F 61(n/a) | null |
| 100 | 01 00v1/空 | null | 100 |
value和typed_value都是用于编码单个值的可选字段。这两个字段中的值必须根据下表进行解释:
value | typed_value | 含义 |
|---|---|---|
| null | null | 值缺失;仅对碎形化对象字段有效 |
| 非 null | null | 值存在,可以是任何类型,包括 null |
| null | 非 null | 值存在,并且是碎形化类型 |
| 非 null | 非 null | 值存在,并且是部分碎形化的对象 |
当value是一个对象且typed_value是一个碎形化对象时,该对象被称为部分碎形化。写入器不得生成value和typed_value都非 null 的数据,除非 Variant 值是一个对象。
如果在需要值的上下文中缺少 Variant,读取器必须返回一个 Variant null (00):基本类型 0 (primitive) 和物理类型 0 (null)。例如,如果需要一个 Variant(如上面的measurement),并且value和typed_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_ARRAY[len=16] | UUID |
| array | GROUP; 见下面的数组 | LIST |
| object | GROUP; 见下面的对象 |
基本类型
基本类型值可以使用上表中等效的 Parquet 基本类型作为typed_value进行碎形化。
除非该值作为对象被碎形化(参见对象),否则typed_value或value(但不能同时)必须非 null。
数组
数组可以通过为typed_value使用 3 级 Parquet 列表进行碎形化。
如果该值不是数组,则typed_value必须为 null。如果该值是数组,则value必须为 null。
列表element必须是一个 required group。elementgroup 可以包含value和typed_value字段。当typed_value不存在或无法表示元素时,元素的value字段将元素存储为 Variant 编码的binary。当不将元素碎形化为特定类型时,可以省略typed_value字段。当将元素碎形化为特定类型时,可以省略value字段。但是,这两个字段中至少必须存在一个。
例如,一个tagsVariant 可以使用以下定义碎形化为一个字符串列表:
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_value或value(但不能同时)必须非 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_valuegroup 中的每个碎形化字段都表示为一个 required group,其中包含可选的value和typed_value字段。当typed_value无法表示该字段时,value字段将该值存储为 Variant 编码的binary。这种布局使读取器能够基于value和typed_value的字段统计信息跳过数据。当不将字段碎形化为特定类型时,可以省略typed_value字段。
部分碎形化对象的value列绝不能包含由typed_value中的 Parquet 列表示的字段(碎形化字段)。读取器可以始终假定数据被正确写入,并且typed_value中的碎形化字段不会出现在value中。因此,当一个字段同时定义在value和碎形化字段typed_value中时,读取结果可能不一致。
例如,一个 Variantevent字段可以使用以下定义碎形化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。
字段的value和typed_value被设置为 null(缺失),以指示该字段在 variant 中不存在。要编码一个存在但值为 null 的字段,value必须包含一个 Variant null:基本类型 0 (primitive) 和物理类型 0 (null)。
当一个字段的value和typed_value都非 null 时,引擎应该失败。如果引擎选择在这种情况下读取,则必须使用typed_value列。读取器可以始终假定数据被正确写入,并且只定义了value或typed_value中的一个。因此,当value和typed_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 且包含碎形化字段时,读取器必须返回一个对象。
嵌套
与任何 Variantvalue字段关联的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 NULL和IS NAN/IS NOT NAN的过滤结果也是有效的。
与其他类型值的比较不一定有效,不应跳过数据。
Variant 的类型转换行为委托给处理引擎。例如,将字符串解释为时间戳可能取决于引擎的 SQL 会话时区。
重构碎形化 Variant
可以使用递归算法恢复未碎形化的 Variant 值,其中初始调用使用顶层 Variant group 字段调用construct_variant。
defconstruct_variant(metadata:Metadata,value:Variant,typed_value:Any)->Variant:"""从 value 和 typed_value 构造 Variant"""iftyped_valueisnotNone:ifisinstance(typed_value,dict):# 这是一个碎形化对象object_fields={name:construct_variant(metadata,field.value,field.typed_value)for(name,field)intyped_value}ifvalueisnotNone:# 这是一个部分碎形化对象assertisinstance(value,VariantObject),"部分碎形化的值必须是一个对象"asserttyped_value.keys().isdisjoint(value.keys()),"对象键必须不重叠"# 合并碎形化字段和非碎形化字段# (字段 ID 和偏移量必须按对应字段名的顺序排列,# 按字典顺序排序(UTF-8 的无符号字节顺序))returnVariantObject(metadata,object_fields).union(VariantObject(metadata,value))else:returnVariantObject(metadata,object_fields)elifisinstance(typed_value,list):# 这是一个碎形化数组assertvalueisNone,"碎形化数组不得与 variant 值冲突"elements=[construct_variant(metadata,elem.value,elem.typed_value)foreleminlist(typed_value)]returnVariantArray(metadata,elements)else:# 这是一个碎形化基本类型assertvalueisNone,"碎形化基本类型不得与 variant 值冲突"returnprimitive_to_variant(typed_value)elifvalueisnotNone:returnVariant(metadata,value)else:# value 缺失returnNonedefprimitive_to_variant(typed_value:Any):Variant:ifisinstance(typed_value,int):returnVariantInteger(typed_value)elifisinstance(typed_value,str):returnVariantString(typed_value)...向后和向前兼容性
碎形化是 Variant 的一个可选特性,读取器必须能够继续读取仅包含value和metadata字段的 group。
不写入碎形化值的引擎必须能够根据此规范读取碎形化值,或者必须失败。
不同的文件可能包含冲突的碎形化 schema。也就是说,文件可能为同一个 Variant 包含具有不兼容类型的不同typed_value列。可能无法推断或指定一个单一的碎形化 schema,使得无需将值重构为 Variant 即可读取表的所有 Parquet 文件。
