Arrow 与 Parquet 第二部分:使用结构体和列表处理嵌套和分层数据


已发布 2022 年 10 月 8 日
作者 tustvold 和 alamb

引言

这是由三部分组成的系列文章的第二篇,探讨了诸如 Rust Apache Arrow 之类的项目如何支持 Apache ArrowApache Parquet 之间的转换。 第一篇文章 涵盖了数据存储和有效性编码的基础知识,本文将涵盖更复杂的 StructList 类型。

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 的情况,它包含两个可为空的级别 dd2

定义级别 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 选择以分层方式将其表示为 ListArrayListArray 包含一个称为 *偏移量* 的单调递增整数列表,一个如果列表可为空的有效性掩码,以及一个包含列表元素的子数组。偏移量数组中每对连续的元素标识 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 实现。我们期待看到您使用它构建的内容!