Arrow 與 Parquet 第二部分:使用 Structs 和 Lists 的巢狀與階層式資料


已發布 2022 年 10 月 08 日
作者 tustvold 和 alamb

簡介

這是三部分系列的第二篇,探討像是 Rust Apache Arrow 等專案如何支援 Apache ArrowApache Parquet 之間的轉換。第一篇文章 涵蓋了資料儲存和有效性編碼的基礎知識,而這篇文章將涵蓋更複雜的 StructList 類型。

Apache Arrow 是一種開放、與語言無關的欄狀記憶體格式,適用於平面和階層式資料,並為高效的分析操作而組織。Apache Parquet 是一種開放、面向欄的資料檔案格式,旨在實現非常高效的資料編碼和檢索。

Struct / Group Columns (結構體/群組欄位)

Parquet 和 Arrow 都有 struct 欄位的概念,這是一種在具名欄位中包含一個或多個其他欄位的欄位,類似於 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
  }
}

這種格式的文件可以儲存在具有以下 schema 的 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,使用父子關係,並在每個可為 Null 的個別陣列上使用獨立的有效性遮罩

  ┌───────────────────┐        ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
  │                   │           ┌─────────────────┐ ┌────────────┐
  │ ┌─────┐   ┌─────┐ │        │  │┌─────┐   ┌─────┐│ │  ┌─────┐   │ │
  │ │  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 並非以結構化方式編碼有效性,而是僅儲存每個原始欄位的定義層級,也就是不包含其他欄位的欄位。給定元素的定義層級是其在 schema 中完全定義的深度。

例如,考慮 d.d2 的情況,它包含兩個可為 Null 的層級 dd2

定義層級為 0 表示在 d 層級為 Null

{
}

定義層級為 1 表示在 d.d2 層級為 Null

{
  "d": { }
}

定義層級為 2 表示 d.d2 的值已定義

{
  "d": { "d2": .. }
}

回到上面的三個 JSON 文件,它們可以使用以下 schema 儲存在 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"                                      │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─

List / Repeated Columns (列表/重複欄位)

支援巢狀類型的最後一項是列表,其中包含可變數量的其他值。例如,以下四個文件各有一個 (可為 Null) 欄位 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 schema 中

Field(name: "a", nullable: true, datatype: List(
  Field(name: "element", nullable: true, datatype: Int32),
)

與之前一樣,Arrow 選擇以階層方式將其表示為 ListArrayListArray 包含一個單調遞增整數列表,稱為偏移量 (offsets)、一個有效性遮罩 (如果列表可為 Null),以及一個包含列表元素的子陣列。偏移量陣列中每對連續的元素都識別出 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 schema 儲存

message schema {
  optional group a (LIST) {
    repeated group list {
      optional int32 element;
    }
  }
}

為了編碼列表,Parquet 除了定義層級之外,還儲存一個整數重複層級。重複層級識別目前值要插入到重複欄位階層結構中的哪個位置。值 0 表示最頂層重複列表中的新列表,值 1 表示最頂層重複列表中的新元素,值 2 表示第二層頂層重複列表中的新元素,依此類推。

這種編碼的一個結果是,repetition 層級中零的數量是欄中總行數,並且欄中的第一個層級必須為 0。

每個重複欄位也都具有對應的定義層級,但是,在這種情況下,它們不是表示 Null 值,而是表示空陣列。

因此,上面的範例將被編碼為

┌─────────────────────────────────────┐
│  ┌─────┐      ┌─────┐               │
│  │  3  │      │  0  │               │
│  ├─────┤      ├─────┤               │
│  │  0  │      │  0  │               │
│  ├─────┤      ├─────┤      ┌─────┐  │
│  │  1  │      │  0  │      │  1  │  │
│  ├─────┤      ├─────┤      ├─────┤  │
│  │  2  │      │  0  │      │  2  │  │
│  ├─────┤      ├─────┤      └─────┘  │
│  │  3  │      │  1  │               │
│  └─────┘      └─────┘               │
│                                     │
│ Definition  Repetition      Values  │
│   Levels      Levels                │
│  "a"                                │
│                                     │
└─────────────────────────────────────┘

接下來:任意巢狀結構:Structs 列表和 Lists 的 Structs

在我們的最後一篇部落格文章中,我們將說明 Parquet 和 Arrow 如何結合這些概念來支援可能為 Null 的資料結構的任意巢狀結構。

如果您想儲存和處理結構化類型,您會很高興聽到 Rust parquet 實作完全支援直接讀取和寫入 Arrow,就像任何其他類型一樣簡單。所有複雜的記錄切分和重建都會自動處理。憑藉此功能和其他令人興奮的功能,例如從 物件儲存 非同步讀取,以及進階的列篩選下推,它是最快且功能最完整的 Rust parquet 實作。我們期待看到您使用它構建什麼!