Arrow 和 Parquet 第二部分:使用结构体和列表的嵌套和分层数据
已发布 2022年10月08日
作者 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
级别的 null
{
}
定义级别 1
将暗示 d.d2
级别的 null
{
"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
包含一个单调递增的整数列表,称为 *offsets*,一个有效性掩码(如果列表可为空),以及一个包含列表元素的子数组。 偏移数组中每对连续元素标识 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 实现。 我们期待看到您使用它构建什么!