Arrow 與 Parquet 第 3 部分:使用結構列表和列表結構的任意巢狀結構


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

介紹

這是三部分系列的第三部分,探討諸如 Rust Apache Arrow 等專案如何支援在 Apache Arrow(用於記憶體內處理)和 Apache Parquet(用於高效儲存)之間進行轉換。Apache Arrow 是一種開放、與語言無關的欄狀記憶體格式,適用於平面和階層式資料,並為高效的分析操作而組織。Apache Parquet 是一種開放、面向欄的資料檔案格式,旨在實現非常高效的資料編碼和檢索。

Arrow 與 Parquet 第 1 部分:基本類型和可空性 涵蓋了基本類型的基礎知識。Arrow 與 Parquet 第 2 部分:使用結構和列表的巢狀和階層式資料 涵蓋了 StructList 類型。這篇文章建立在這個基礎之上,展示了這兩種格式如何結合這些類型來支援任意巢狀結構。

某些函式庫,例如 Rust parquet 實作,為此類組合提供了完整的支援,這些函式庫的使用者無需擔心這些細節,除非是為了滿足他們自己的好奇心。其他函式庫可能無法處理某些邊角案例,而這篇文章說明了為什麼這樣做如此複雜。

具有列表的結構

考慮以下三個 JSON 文件

{                     # <-- First record
  "a": [1],           # <-- top-level field a containing list of integers
  "b": [              # <-- top-level field b containing list of structures
    {                 # <-- list element of b containing two field b1 and b2
      "b1": 1         # <-- b1 is always provided (non nullable)
    },
    {
      "b1": 1,
      "b2": [         # <-- b2 contains list of integers
        3, 4          # <-- list elements of b.b2 always provided (non nullable)
      ]
    }
  ]
}
{
  "b": [              # <-- b is always provided (non nullable)
    {
      "b1": 2
    },
  ]
}
{
  "a": [null, null],  # <-- list elements of a are nullable
  "b": [null]         # <-- list elements of b are nullable
}

這種格式的文件可以儲存在此 Arrow Schema 中

Field(name: "a", nullable: true, datatype: List(
  Field(name: "element", nullable: true, datatype: Int32),
)
Field(name: "b"), nullable: false, datatype: List(
  Field(name: "element", nullable: true, datatype: Struct[
    Field(name: "b1", nullable: false, datatype: Int32),
    Field(name: "b2", nullable: true, datatype: List(
      Field(name: "element", nullable: false, datatype: Int32)
    ))
  ])
))

如先前所述,Arrow 選擇以階層式方式表示它。StructArrays 儲存為子陣列,其中包含結構的每個欄位。ListArrays 儲存為單調遞增整數的列表(稱為偏移量),值儲存在單個子陣列中。偏移量陣列中每對連續的元素都標識了該陣列索引的子陣列切片。

範例的 Arrow 編碼將是

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
                     ┌──────────────────┐
│ ┌─────┐   ┌─────┐  │ ┌─────┐   ┌─────┐│ │
  │  1  │   │  0  │  │ │  1  │   │  1  ││
│ ├─────┤   ├─────┤  │ ├─────┤   ├─────┤│ │
  │  0  │   │  1  │  │ │  0  │   │ ??  ││
│ ├─────┤   ├─────┤  │ ├─────┤   ├─────┤│ │
  │  1  │   │  1  │  │ │  0  │   │ ??  ││
│ └─────┘   ├─────┤  │ └─────┘   └─────┘│ │
            │  3  │  │ Validity   Values│
│ Validity  └─────┘  │                  │ │
                     │ child[0]         │
│ "a"       Offsets  │ PrimitiveArray   │ │
  ListArray          └──────────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
           ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│                    ┌──────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
  ┌─────┐  │ ┌─────┐ │ ┌─────┐  │   ┌─────┐ ┌─────┐ ┌──────────┐ │ │ │
│ │  0  │    │  1  │ │ │  1  │  │ │ │  0  │ │  0  │ │ ┌─────┐  │
  ├─────┤  │ ├─────┤ │ ├─────┤  │   ├─────┤ ├─────┤ │ │  3  │  │ │ │ │
│ │  2  │    │  1  │ │ │  1  │  │ │ │  1  │ │  0  │ │ ├─────┤  │
  ├─────┤  │ ├─────┤ │ ├─────┤  │   ├─────┤ ├─────┤ │ │  4  │  │ │ │ │
│ │  3  │    │  1  │ │ │  2  │  │ │ │  0  │ │  2  │ │ └─────┘  │
  ├─────┤  │ ├─────┤ │ ├─────┤  │   ├─────┤ ├─────┤ │          │ │ │ │
│ │  4  │    │  0  │ │ │ ??  │  │ │ │ ??  │ │  2  │ │  Values  │
  └─────┘  │ └─────┘ │ └─────┘  │   └─────┘ ├─────┤ │          │ │ │ │
│                    │          │ │         │  2  │ │          │
  Offsets  │ Validity│ Values   │           └─────┘ │          │ │ │ │
│                    │          │ │Validity         │ child[0] │
           │         │ "b1"     │           Offsets │ Primitive│ │ │ │
│                    │ Primitive│ │ "b2"            │ Array    │
           │         │ Array    │   ListArray       └──────────┘ │ │ │
│                    └──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
           │ "element"                                             │ │
│            StructArray
  "b"      └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ ListArray
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

這種格式的文件可以儲存在此 Parquet Schema 中

message schema {
  optional group a (LIST) {
    repeated group list {
      optional int32 element;
    }
  }
  required group b (LIST) {
    repeated group list {
      optional group element {
        required int32 b1;
        optional group b2 (LIST) {
          repeated group list {
            required int32 element;
          }
        }
      }
    }
  }
}

如我們之前的文章中所述,Parquet 使用重複層級和定義層級來編碼巢狀結構和可空性。

定義和重複層級是一個不簡單的主題。如需更多詳細資訊,您可以閱讀 Google Dremel Paper,其中提供了該演算法的學術描述。您也可以瀏覽這個 gist 以查看 Rust parquet 程式碼,該程式碼生成了以下範例。

範例的 Parquet 編碼將是

┌───────────────────────────────┐ ┌────────────────────────────────┐
│ ┌─────┐    ┌─────┐    ┌─────┐ │ │  ┌─────┐    ┌─────┐    ┌─────┐ │
│ │  3  │    │  0  │    │  1  │ │ │  │  2  │    │  0  │    │  1  │ │
│ ├─────┤    ├─────┤    └─────┘ │ │  ├─────┤    ├─────┤    ├─────┤ │
│ │  0  │    │  0  │            │ │  │  2  │    │  1  │    │  1  │ │
│ ├─────┤    ├─────┤      Data  │ │  ├─────┤    ├─────┤    ├─────┤ │
│ │  2  │    │  0  │            │ │  │  2  │    │  0  │    │  2  │ │
│ ├─────┤    ├─────┤            │ │  ├─────┤    ├─────┤    └─────┘ │
│ │  2  │    │  1  │            │ │  │  1  │    │  0  │            │
│ └─────┘    └─────┘            │ │  └─────┘    └─────┘     Data   │
│                               │ │                                │
│Definition Repetition          │ │ Definition Repetition          │
│  Levels     Levels            │ │   Levels     Levels            │
│                               │ │                                │
│ "a"                           │ │  "b.b1"                        │
└───────────────────────────────┘ └────────────────────────────────┘

┌───────────────────────────────┐
│  ┌─────┐    ┌─────┐    ┌─────┐│
│  │  2  │    │  0  │    │  3  ││
│  ├─────┤    ├─────┤    ├─────┤│
│  │  4  │    │  1  │    │  4  ││
│  ├─────┤    ├─────┤    └─────┘│
│  │  4  │    │  2  │           │
│  ├─────┤    ├─────┤           │
│  │  2  │    │  0  │           │
│  ├─────┤    ├─────┤     Data  │
│  │  1  │    │  0  │           │
│  └─────┘    └─────┘           │
│Definition  Repetition         │
│  Levels      Levels           │
│                               │
│  "b.b2"                       │
└───────────────────────────────┘

其他複雜性

這個系列的文章必然忽略了許多細節,這些細節進一步使實際的實作變得複雜。

  • ListArray 可能包含一個非空的偏移量範圍,該範圍被有效性遮罩遮蔽。
  • 從可空欄位讀取給定數量的列需要讀取定義層級,並根據存在的 null 數量確定要讀取的值的數量。
  • 從重複欄位讀取給定數量的列需要讀取重複層級,並根據重複層級為 0 偵測新列。
  • 一個 Parquet 檔案可能包含多個列組,每個列組包含多個列區塊。
  • 一個列區塊可能包含多個頁面,並且跨列的頁面之間沒有關聯性。
  • Parquet 具有用於表示具有不同程度可空性的列表的替代 Schema。
  • 還有更多...

總結

Parquet 和 Arrow 都是欄狀格式,並支援巢狀結構和列表,但是,它們表示這種巢狀結構的方式差異很大,並且兩種格式之間的轉換很複雜。

幸運的是,使用 Rust parquet 實作,在 Arrow、Parquet 中讀寫巢狀資料或在這兩者之間進行轉換,就像讀取非巢狀資料一樣簡單。該函式庫會自動處理所有複雜的記錄切分和重建。憑藉此功能和其他令人興奮的功能,例如支援從 物件儲存 非同步讀取,它是目前最快且功能最完整的 Rust parquet 實作。我們期待看到您使用它構建什麼!