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 部分:使用結構和列表的巢狀和階層式資料 涵蓋了 Struct
和 List
類型。這篇文章建立在這個基礎之上,展示了這兩種格式如何結合這些類型來支援任意巢狀結構。
某些函式庫,例如 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 選擇以階層式方式表示它。StructArray
s 儲存為子陣列,其中包含結構的每個欄位。ListArray
s 儲存為單調遞增整數的列表(稱為偏移量),值儲存在單個子陣列中。偏移量陣列中每對連續的元素都標識了該陣列索引的子陣列切片。
範例的 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 實作。我們期待看到您使用它構建什麼!