Arrow 和 Parquet 第二部分:使用结构体和列表的嵌套和分层数据
已发布 2022 年 10 月 8 日
作者: tustvold 和 alamb
简介
这是三部分系列文章中的第二篇,探讨了诸如 Rust Apache Arrow 等项目如何支持 Apache Arrow 和 Apache Parquet 之间的转换。第一篇文章介绍了数据存储和有效性编码的基础知识,而本文将介绍更复杂的 Struct 和 List 类型。
Apache Arrow 是一种开放的、语言无关的列式内存格式,用于平面和分层数据,旨在实现高效的分析操作。Apache Parquet 是一种开放的、面向列的数据文件格式,专为高效的数据编码和检索而设计。
结构体/组列
Parquet 和 Arrow 都有结构体列的概念,它是一种包含一个或多个其他列的命名字段的列,类似于 JSON 对象。
例如,考虑以下三个 JSON 文档
{ # <-- First record
"a": 1, # <-- the top level fields are a, b, c, and d
"b": { # <-- b is always provided (not nullable)
"b1": 1, # <-- b1 and b2 are "nested" fields of "b"
"b2": 3 # <-- b2 is always provided (not nullable)
},
"d": {
"d1": 1 # <-- d1 is a "nested" field of "d"
}
}
{ # <-- Second record
"a": 2,
"b": {
"b2": 4 # <-- note "b1" is NULL in this record
},
"c": { # <-- note "c" was NULL in the first record
"c1": 6 but when "c" is provided, c1 is also
}, always provided (not nullable)
"d": {
"d1": 2,
"d2": 1
}
}
{ # <-- Third record
"b": {
"b1": 5,
"b2": 6
},
"c": {
"c1": 7
}
}
这种格式的文档可以使用此模式存储在 Arrow StructArray 中
Field(name: "a", nullable: true, datatype: Int32)
Field(name: "b", nullable: false, datatype: Struct[
Field(name: "b1", nullable: true, datatype: Int32),
Field(name: "b2", nullable: false, datatype: Int32)
])
Field(name: "c"), nullable: true, datatype: Struct[
Field(name: "c1", nullable: false, datatype: Int32)
])
Field(name: "d"), nullable: true, datatype: Struct[
Field(name: "d1", nullable: false, datatype: Int32)
Field(name: "d2", nullable: true, datatype: Int32)
])
Arrow 使用父子关系以分层方式表示每个 StructArray,每个单独的可为空数组上都有独立的有效性掩码
┌───────────────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ │ ┌─────────────────┐ ┌────────────┐
│ ┌─────┐ ┌─────┐ │ │ │┌─────┐ ┌─────┐│ │ ┌─────┐ │ │
│ │ 1 │ │ 1 │ │ ││ 1 │ │ 1 ││ │ │ 3 │ │
│ ├─────┤ ├─────┤ │ │ │├─────┤ ├─────┤│ │ ├─────┤ │ │
│ │ 1 │ │ 2 │ │ ││ 0 │ │ ?? ││ │ │ 4 │ │
│ ├─────┤ ├─────┤ │ │ │├─────┤ ├─────┤│ │ ├─────┤ │ │
│ │ 0 │ │ ?? │ │ ││ 1 │ │ 5 ││ │ │ 6 │ │
│ └─────┘ └─────┘ │ │ │└─────┘ └─────┘│ │ └─────┘ │ │
│ Validity Values │ │Validity Values│ │ Values │
│ │ │ │ │ │ │ │
│ "a" │ │"b.b1" │ │ "b.b2" │
│ PrimitiveArray │ │ │PrimitiveArray │ │ Primitive │ │
└───────────────────┘ │ │ │ Array │
│ └─────────────────┘ └────────────┘ │
"b"
│ StructArray │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌───────────┐ ┌──────────┐┌─────────────────┐ │
│ ┌─────┐ │ ┌─────┐ │ │ │ ┌─────┐ │┌─────┐ ││ ┌─────┐ ┌─────┐│
│ 0 │ │ │ ?? │ │ │ 1 │ ││ 1 │ ││ │ 0 │ │ ?? ││ │
│ ├─────┤ │ ├─────┤ │ │ │ ├─────┤ │├─────┤ ││ ├─────┤ ├─────┤│
│ 1 │ │ │ 6 │ │ │ 1 │ ││ 2 │ ││ │ 1 │ │ 1 ││ │
│ ├─────┤ │ ├─────┤ │ │ │ ├─────┤ │├─────┤ ││ ├─────┤ ├─────┤│
│ 1 │ │ │ 7 │ │ │ 0 │ ││ ?? │ ││ │ ?? │ │ ?? ││ │
│ └─────┘ │ └─────┘ │ │ │ └─────┘ │└─────┘ ││ └─────┘ └─────┘│
Validity │ Values │ Validity │ Values ││ Validity Values│ │
│ │ │ │ │ │ ││ │
│ "c.c1" │ │"d.d1" ││ "d.d2" │ │
│ │ Primitive │ │ │ │Primitive ││ PrimitiveArray │
│ Array │ │Array ││ │ │
│ └───────────┘ │ │ └──────────┘└─────────────────┘
"c" "d" │
│ StructArray │ │ StructArray
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
更多技术细节可在StructArray 格式规范中找到。
定义级别
与 Arrow 不同,Parquet 不以结构化方式编码有效性,而是仅为每个原始列(即不包含其他列的列)存储定义级别。给定元素的定义级别是它在模式中完全定义的深度。
例如,考虑 d.d2 的情况,它包含两个可为空级别 d 和 d2。
定义级别为 0 意味着 d 级别为空
{
}
定义级别为 1 意味着 d.d2 级别为空
{
"d": { }
}
定义级别为 2 意味着 d.d2 的值为已定义
{
"d": { "d2": .. }
}
回到上面三个 JSON 文档,它们可以使用此模式存储在 Parquet 中
message schema {
optional int32 a;
required group b {
optional int32 b1;
required int32 b2;
}
optional group c {
required int32 c1;
}
optional group d {
required int32 d1;
optional int32 d2;
}
}
示例的 Parquet 编码将是
┌────────────────────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ┌─────┐ ┌─────┐ │ ┌──────────────────────┐ ┌───────────┐ │
│ │ 1 │ │ 1 │ │ │ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ │
│ ├─────┤ ├─────┤ │ │ │ 1 │ │ 1 │ │ │ │ 3 │ │ │
│ │ 1 │ │ 2 │ │ │ │ ├─────┤ ├─────┤ │ │ ├─────┤ │
│ ├─────┤ └─────┘ │ │ │ 0 │ │ 5 │ │ │ │ 4 │ │ │
│ │ 0 │ │ │ │ ├─────┤ └─────┘ │ │ ├─────┤ │
│ └─────┘ │ │ │ 1 │ │ │ │ 6 │ │ │
│ │ │ │ └─────┘ │ │ └─────┘ │
│ Definition Data │ │ │ │ │ │
│ Levels │ │ │ Definition Data │ │ Data │
│ │ │ Levels │ │ │ │
│ "a" │ │ │ │ │ │
└────────────────────────┘ │ "b.b1" │ │ "b.b2" │ │
│ └──────────────────────┘ └───────────┘
"b" │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌ ─ ─ ─ ─ ─ ── ─ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌────────────────────┐ │ ┌────────────────────┐ ┌──────────────────┐ │
│ │ ┌─────┐ ┌─────┐ │ │ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │
│ │ 0 │ │ 6 │ │ │ │ │ 1 │ │ 1 │ │ │ │ 1 │ │ 1 │ │ │
│ │ ├─────┤ ├─────┤ │ │ │ ├─────┤ ├─────┤ │ │ ├─────┤ └─────┘ │
│ │ 1 │ │ 7 │ │ │ │ │ 1 │ │ 2 │ │ │ │ 2 │ │ │
│ │ ├─────┤ └─────┘ │ │ │ ├─────┤ └─────┘ │ │ ├─────┤ │
│ │ 1 │ │ │ │ │ 0 │ │ │ │ 0 │ │ │
│ │ └─────┘ │ │ │ └─────┘ │ │ └─────┘ │
│ │ │ │ │ │ │ │
│ │ Definition Data │ │ │ Definition Data │ │ Definition Data │
│ Levels │ │ │ Levels │ │ Levels │ │
│ │ │ │ │ │ │ │
│ "c.c1" │ │ │ "d.d1" │ │ "d.d2" │ │
│ └────────────────────┘ │ └────────────────────┘ └──────────────────┘
"c" │ "d" │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
列表/重复列
嵌套类型的支持以列表结束,列表包含可变数量的其他值。例如,以下四个文档各自有一个包含整数列表的(可为空)字段 a
{ # <-- First record
"a": [1], # <-- top-level field a containing list of integers
}
{ # <-- "a" is not provided (is null)
}
{ # <-- "a" is non-null but empty
"a": []
}
{
"a": [null, 2], # <-- "a" has a null and non-null elements
}
这种格式的文档可以存储在此 Arrow 模式中
Field(name: "a", nullable: true, datatype: List(
Field(name: "element", nullable: true, datatype: Int32),
)
如前所述,Arrow 选择以分层方式将其表示为 ListArray。ListArray 包含一个单调递增的整数列表,称为偏移量,如果列表可为空,则包含一个有效性掩码,以及一个包含列表元素的子数组。偏移量数组中的每对连续元素标识 ListArray 中该索引的子数组的一个切片
例如,偏移量为 [0, 2, 3, 3] 的列表包含 3 对偏移量,(0,2)、(2,3) 和 (3,3),因此表示一个长度为 3 的 ListArray,其值如下
0: [child[0], child[1]]
1: [child[2]]
2: []
对于上面有 4 个 JSON 文档的示例,这将以 Arrow 编码为
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌──────────────────┐ │
│ ┌─────┐ ┌─────┐ │ ┌─────┐ ┌─────┐│
│ 1 │ │ 0 │ │ │ 1 │ │ 1 ││ │
│ ├─────┤ ├─────┤ │ ├─────┤ ├─────┤│
│ 0 │ │ 1 │ │ │ 0 │ │ ?? ││ │
│ ├─────┤ ├─────┤ │ ├─────┤ ├─────┤│
│ 1 │ │ 1 │ │ │ 1 │ │ 2 ││ │
│ ├─────┤ ├─────┤ │ └─────┘ └─────┘│
│ 1 │ │ 1 │ │ Validity Values│ │
│ └─────┘ ├─────┤ │ │
│ 3 │ │ child[0] │ │
│ Validity └─────┘ │ PrimitiveArray │
│ │ │
│ Offsets └──────────────────┘
"a" │
│ ListArray
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
更多技术细节可在ListArray 格式规范中找到。
Parquet 重复级别
上面有 4 个 JSON 文档的示例可以存储在此 Parquet 模式中
message schema {
optional group a (LIST) {
repeated group list {
optional int32 element;
}
}
}
为了编码列表,Parquet 除了定义级别外,还存储一个整数重复级别。重复级别标识当前值应插入到重复字段层次结构中的位置。值为 0 意味着在最顶层重复列表中有一个新列表,值为 1 意味着在最顶层重复列表中有一个新元素,值为 2 意味着在第二层最顶层重复列表中有一个新元素,依此类推。
这种编码的一个结果是 repetition 级别中零的数量是列中的总行数,并且列中的第一级别必须为 0。
每个重复字段也有一个相应的定义级别,但是,在这种情况下,它们表示空数组,而不是空值。
因此,上面的示例将编码为
┌─────────────────────────────────────┐
│ ┌─────┐ ┌─────┐ │
│ │ 3 │ │ 0 │ │
│ ├─────┤ ├─────┤ │
│ │ 0 │ │ 0 │ │
│ ├─────┤ ├─────┤ ┌─────┐ │
│ │ 1 │ │ 0 │ │ 1 │ │
│ ├─────┤ ├─────┤ ├─────┤ │
│ │ 2 │ │ 0 │ │ 2 │ │
│ ├─────┤ ├─────┤ └─────┘ │
│ │ 3 │ │ 1 │ │
│ └─────┘ └─────┘ │
│ │
│ Definition Repetition Values │
│ Levels Levels │
│ "a" │
│ │
└─────────────────────────────────────┘
接下来:任意嵌套:结构体列表和列表结构体
在我们的最后一篇博客文章中,我们解释了 Parquet 和 Arrow 如何结合这些概念来支持可能为空的数据结构的任意嵌套。
如果您想存储和处理结构化类型,您会很高兴地听到 Rust parquet 实现完全支持直接读取和写入 Arrow,就像任何其他类型一样简单。所有复杂的记录分解和重构都会自动处理。凭借此功能以及从对象存储异步读取等其他令人兴奋的功能,以及高级行过滤器下推,它是最快、功能最完善的 Rust parquet 实现。我们期待看到您用它构建什么!