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" │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
列表 / 重复列
结束对嵌套类型的支持的是 *列表*,它包含可变数量的其他值。例如,以下四个文档 each have a (nullable) field 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
值表示第二个最顶层重复列表中的新元素,依此类推。
这种编码的结果是 重复
级别中零的个数是列中的总行数,并且列中的第一个级别必须为 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 实现。我们期待看到您使用它构建的内容!