iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0

在 Apache Arrow 中,一張資料表不是一個模糊的「表格物件」,而是由三個核心概念組成:

Schema
→ 定義欄位名稱、資料型別與欄位順序

Array
→ 儲存單一欄位、相同型別的一串資料

RecordBatch
→ 將多個等長 Array 組成一批欄式表格資料

可以把它們想成:

Schema 是表格的設計圖
Array 是每一欄實際存放的資料
RecordBatch 是一小批可以交給 Query Engine 處理的表格

Day 8 提到,DataFusion 使用 Arrow 作為記憶體中的欄式資料格式。今天要進一步拆解:當資料進入記憶體後,它究竟長什麼樣子?


Schema、Array 與 RecordBatch 的關係

假設我們有一張訂單表:

order_id city amount is_member
1 台北 1200.0 true
2 台中 850.0 false
3 台北 2300.0 true

在 Arrow 中,這張表可以拆成 Schema 與多個 Array,最後再組合成一個 RecordBatch。

Apache Arrow 的核心資料結構:Schema、Array 與 RecordBatch

圖 1:Apache Arrow 中 Schema、Array 與 RecordBatch 的關係。圖由本文整理。

接下來分別拆解這三個核心資料結構。


Schema:資料的規格書

Schema 描述一批資料「應該長什麼樣子」。

以上面的訂單資料為例,Schema 可以概念化成:

order_id  : Int64
city      : Utf8
amount    : Float64
is_member : Boolean

Schema 至少告訴我們三件事:

  • 欄位名稱
  • 每個欄位的資料型別
  • 欄位的順序

因此,Schema 不會保存實際訂單資料;它描述的是資料的結構與規則。

可以把它想成資料表的設計圖:

Schema
├── order_id:64 位元整數
├── city:UTF-8 字串
├── amount:64 位元浮點數
└── is_member:布林值

當 Query Engine 讀取 CSV、JSON 或 Parquet 時,需要知道每個欄位的型別,才能正確進行篩選、比較、聚合與 Join。

例如:

SELECT SUM(amount)
FROM orders;

SUM() 需要知道 amount 是可以計算的數值型別;若它其實是字串,就需要先轉型,或直接回傳錯誤。


Array:一個欄位的一串值

在 Arrow 裡,Array 是一串已知長度、而且型別相同的值。

例如訂單資料的 city 欄位:

city Array
[台北, 台中, 台北]

amount 欄位則是另一個 Array:

amount Array
[1200.0, 850.0, 2300.0]

每個欄位都會有自己的 Array:

order_id Array  : [1, 2, 3]
city Array      : [台北, 台中, 台北]
amount Array    : [1200.0, 850.0, 2300.0]
is_member Array : [true, false, true]

這就是 Arrow 的欄式資料表示法:同一欄位的值會集中在一起,而不是將每筆完整資料排在一起。

Row-based
[1, 台北, 1200.0, true]
[2, 台中, 850.0, false]
[3, 台北, 2300.0, true]

Arrow 的 Columnar 表示
order_id  : [1, 2, 3]
city      : [台北, 台中, 台北]
amount    : [1200.0, 850.0, 2300.0]
is_member : [true, false, true]

這也呼應 Day 7 的內容:分析查詢通常只需要其中幾個欄位,因此欄式資料可以讓 Query Engine 集中處理真正需要的資料。


RecordBatch:一小批欄式表格資料

單一 Array 只能表示一個欄位;一張表格則需要多個欄位。

Arrow 會將多個 Array 組合成一個 RecordBatch

RecordBatch
├── order_id Array  : [1, 2, 3]
├── city Array      : [台北, 台中, 台北]
├── amount Array    : [1200.0, 850.0, 2300.0]
└── is_member Array : [true, false, true]

這裡有一個很重要的規則:

同一個 RecordBatch 中的所有 Array,長度必須相同。

因為各欄位的第 1 個值要組成第 1 列資料:

order_id[0]  = 1
city[0]      = 台北
amount[0]    = 1200.0
is_member[0] = true

合起來才是:

[1, 台北, 1200.0, true]

同樣地,第 2 個位置會組成第 2 列:

[2, 台中, 850.0, false]

所以 RecordBatch 可以視為:

具有共同 Schema 的一小批表格資料。

為什麼不是整張表只用一個 RecordBatch?

實務上的資料量可能很大,一張表可能有數百萬甚至數十億列資料。

如果一次把整張表放進一個 RecordBatch:

一個超大的 RecordBatch

可能會消耗大量記憶體,也不利於串流式處理與平行執行。

因此,資料通常會切成多個批次:

orders
├── RecordBatch 1:前 8,192 列
├── RecordBatch 2:接續的 8,192 列
├── RecordBatch 3:接續的 8,192 列
└── ...

實際批次大小會依資料來源、設定與執行環境而不同;重點是 DataFusion 不需要等待整張表都讀進記憶體,便能開始處理前面的資料批次。

這樣的處理方式有幾個好處:

  • 限制單次需要使用的記憶體
  • 讓資料能一批一批往下游執行節點傳遞
  • 讓不同批次有機會被平行處理
  • 更適合處理大型檔案與持續流入的資料

Query Engine 如何處理 RecordBatch?

回到前幾天使用過的 SQL:

SELECT city, SUM(amount)
FROM orders
WHERE is_member = true
GROUP BY city;

假設 DataFusion 收到一個 RecordBatch:

order_id  : [1, 2, 3]
city      : [台北, 台中, 台北]
amount    : [1200.0, 850.0, 2300.0]
is_member : [true, false, true]

它可以依序進行:

1. Filter
   依 is_member = true 篩選資料

2. Projection
   保留 city 與 amount

3. Aggregate
   依 city 分組,計算 SUM(amount)

篩選後的概念結果是:

city   : [台北, 台北]
amount : [1200.0, 2300.0]

接著再聚合:

台北 → 1200.0 + 2300.0 = 3500.0

這裡的關鍵是:DataFusion 的執行節點會接收或產生 Arrow RecordBatch,而不是一次只處理一筆資料列。


Schema 如何幫助 Query Engine 發現問題?

Schema 也扮演資料品質檢查的角色。

例如某份資料原本預期:

amount : Float64

但某一批資料出現:

amount : "unknown"

這就可能造成型別不符。

或者原本預期有四個欄位:

order_id
city
amount
is_member

但新資料少了 amount 欄位,也會讓後續的 SQL 無法正確執行。

因此,Schema 不只是描述資料的文件,它也是 Query Engine 理解資料、規劃查詢與及早發現問題的重要依據。

這也是資料工程常見的工作:在資料進入下游系統前,確認 Schema 是否符合預期。


NULL 在 Arrow 中怎麼表示?

資料不完整是很常見的情況。

例如某筆訂單還沒有城市資料:

order_id city amount is_member
1 台北 1200.0 true
2 NULL 850.0 false
3 台北 2300.0 true

概念上,city Array 可以表示為:

city: [台北, NULL, 台北]

Arrow 會額外使用一份 Validity Bitmap(有效性位元圖),記錄每個位置的值是否有效:

值:       [台北,  NULL, 台北]
有效性:   [  1,     0,    1]
1:這個位置有有效值
0:這個位置是 NULL

這樣 Arrow 不需要為每個型別都設計一個特殊的 NULL 值,也能用一致的方式處理缺失資料。

目前只要先記住概念即可;實際的 Bitmap、Buffer 與記憶體配置,之後深入 Arrow 的底層結構時再繼續拆解。


Schema、Array 與 RecordBatch 的分工

最後整理一次:

概念 它負責什麼? 例子
Schema 定義欄位名稱、型別與順序 amount: Float64
Array 儲存單一欄位的一串同型別值 [1200.0, 850.0, 2300.0]
RecordBatch 將多個等長 Array 組成一批表格資料 一批 3 列訂單資料

可以用一句話記住:

Schema 告訴我們資料應該長什麼樣子;
Array 保存每個欄位的實際值;
RecordBatch 把各欄位組成可被 Query Engine 處理的一批資料。

今日小結

今天拆解了 Arrow 最重要的三個核心資料結構。

當資料進入 DataFusion 後,會以 Arrow 的欄式形式存在記憶體中:

Schema
↓
定義欄位規格

Array
↓
儲存每一欄資料

RecordBatch
↓
組成一批可運算的表格資料

這種設計讓 DataFusion 能一批一批執行 Filter、Projection、Aggregate 與 Join,也讓欄式資料處理更有效率。

明天會接著追蹤資料讀取流程:從 CSV、JSON、Parquet 等資料來源開始,資料如何一步步進入 DataFusion,最後變成 Arrow RecordBatch。


延伸閱讀


上一篇
Day 8|認識 Apache Arrow:DataFusion 的資料基礎
系列文
深入 SQL 查詢引擎:30 天用 Rust 與 Apache DataFusion 解構資料處理流程9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言