資料分析時,我們常會使用 CSV、Excel、JSON 等資料格式,但是當資料量太大時,如果直接將整個檔案讀取進去,就容易遇到記憶體不足導致當機的問題。這時候我們可以改用支援分段、分欄讀取的 Parquet 格式檔案。
我第一次接觸到 Parquet 是有次想要訓練一個預測伺服器硬碟故障的模型(雖然後來懶了就沒做出來😅),我去 Kaggle 下載了好幾份硬碟故障的 CSV 檔案,用 Pandas 把它們合併,結果一執行電腦就當機了。問了 GPT 才知道原來是資料量太大,記憶體不夠,於是它幫我改成使用 Parquet 格式,就可以不用一次把所有資料讀進記憶體,也就不會一執行程式就當機。
什麼是 Parquet?
如果你把資料想成一張很大的表格(很多列、很多欄),那常見的 CSV / Excel 比較像是「一列一列排好」存起來:你要看其中某一欄,通常還是得把整份資料掃過一遍,資料一大就很容易慢、也容易吃爆記憶體。
Parquet 則是「欄式儲存(columnar storage)」:同一欄的資料會被放在一起。
所以當你只需要幾個欄位(例如只想看 會員ID、訂單金額、下單時間),Parquet 可以更有效率地只讀那些欄位,少讀很多不需要的內容,速度和記憶體用量通常都會更漂亮。
你可以把它想成「衣櫃整理法」:
CSV / Excel:像把每一套衣服(上衣+褲子+外套)整套掛好。你只想拿外套,還是得從整套裡面翻。
Parquet:像把上衣放同一格、褲子放同一格、外套放同一格。你只拿外套那格就好。
Parquet 為什麼特別適合巨量資料?
Parquet 檔案裡面,資料不是「一坨」存著,它有一個很重要的分段結構(這也是它能分段讀取的原因):
Row Group(列群組):把很多列資料切成一段一段的「區塊」。你可以把它當成「分批打包」。
每個 Row Group 裡,每一欄都各自有自己的 Column Chunk(欄區塊)。
Column Chunk 裡又會切成更小的 Page(頁面),Page 通常是壓縮與編碼的最小單位。
檔案最後還會有 metadata(中繼資料),記錄欄位資訊、每個區塊在哪、統計資訊等,讓讀取端更容易「只抓需要的部分」。
這樣的好處很直覺:
只讀需要的欄位(少 I/O、少記憶體)
可以分段讀(例如只讀第 0、1、2 個 row group)
壓縮效果常常比 CSV 好(同一欄型態一致、重複值多時很吃香)
小提醒:Parquet 也不是萬能。因為它為了支援這些能力,會多存一些 metadata,所以資料量很小時,檔案可能反而不見得比 CSV 划算。
在 Go 語言中存取 Parquet
我們要怎麼在 Go 語言裡面讀取和寫入 Parquet 檔呢?
在 Go 的世界裡,如果你想直接操作 Parquet 的底層結構,最主流的一條路是 Apache Arrow 的 Go 套件(github.com/apache/arrow-go/parquet)。它提供了 Parquet 的 reader/writer,而且還有一個很關鍵的能力:你可以指定只讀某些欄位、某些 row groups,非常符合 Parquet 的設計初衷。
但相對地,Arrow 的 API 會比較偏「底層」,你常常要自己處理 schema、row group、欄位對應、型別轉換等等——能做、很強,但寫起來比較囉唆。
而 Insyra 把這些麻煩包起來,保留 Parquet 的強項(分欄/分段/串流),但把操作變得更直覺。
使用 Insyra 的 parquet 套件包
如果你不想花時間跟 Parquet 的底層結構(schema、row group、型別)搏鬥,那這段就是本篇精華:Insyra 直接把 Parquet 包裝成你熟悉的 DataTable / DataList,你要「整份讀」、「只讀幾欄」、「分批讀」、「只拿單一欄」都能用很直覺的方式完成。
本篇範例以 Insyra
v0.2.11為準。
匯入套件
import (
"context"
"github.com/HazelnutParadise/insyra"
"github.com/HazelnutParadise/insyra/isr" // 如果你要用語法糖(建議)
"github.com/HazelnutParadise/insyra/parquet"
)
1) 先看檔案到底多大、有哪些欄位:Inspect
拿到一個 Parquet 檔,第一步通常不是直接讀,而是先「瞄一眼」欄位與規模,才知道要不要分批、要讀哪些欄。
info, err := parquet.Inspect("data.parquet")
if err != nil {
panic(err)
}
println("rows:", info.NumRows)
println("rowGroups:", info.NumRowGroups)
for _, c := range info.Columns {
println("col:", c.Name)
}
2) 直接讀成 DataTable:Read
最簡單的用法:整份讀成 DataTable。
ctx := context.Background()
dt, err := parquet.Read(ctx, "data.parquet", parquet.ReadOptions{})
if err != nil {
panic(err)
}
dt.Show()
但 Parquet 最爽的地方就是「可以只讀你要的」:例如只讀幾個欄位、只讀指定 row groups。
dt, err := parquet.Read(ctx, "data.parquet", parquet.ReadOptions{
Columns: []string{"會員ID", "訂單金額", "下單時間"},
RowGroups: []int{0, 1, 2},
})
if err != nil {
panic(err)
}
3) 巨量資料最推薦:Stream 分批讀
資料真的很大時,請直接用 Stream。它會一批一批吐 DataTable,你可以邊讀邊算、邊篩、邊寫,不用一次把整包塞進記憶體。
ctx := context.Background()
dtCh, errCh := parquet.Stream(ctx, "large.parquet", parquet.ReadOptions{
Columns: []string{"會員ID", "訂單金額"},
}, 50_000) // 每批 5 萬列,視你的機器調整
for {
select {
case dt, ok := <-dtCh:
if !ok {
return
}
// 每次拿到一批就處理
// 例如:累加、做統計、過濾、寫出其他檔案...
_ = dt
case err := <-errCh:
if err != nil {
panic(err)
}
return
}
}
4) 我只要單一欄:ReadColumn → DataList
很多分析其實只需要一欄(例如:金額做分布、年齡做平均),這時候整張表讀進來反而浪費記憶體,也不方便處理。
ctx := context.Background()
amounts, err := parquet.ReadColumn(ctx, "data.parquet", "訂單金額", parquet.ReadColumnOptions{
RowGroups: []int{0, 1},
MaxValues: 1_000_000, // 超過就報錯,避免你不小心把記憶體撐爆
})
if err != nil {
panic(err)
}
println("len:", amounts.Len)
5) 寫回 Parquet:Write
只要你手上有 DataTable(或任何 IDataTable),就能直接寫成 Parquet。
dt := insyra.DT.Of(isr.DLs{
insyra.DL.Of(1, 2, 3).SetName("ID"),
insyra.DL.Of("A", "B", "C").SetName("Name"),
})
if err := parquet.Write(dt, "output.parquet"); err != nil {
panic(err)
}
6) 進階:用 CCL 直接對 Parquet 做篩選/加工
熟悉 Insyra 的朋友(雖然大概沒幾個)應該都知道,Insyra 有一個很強大的功能 — CCL(Column Calculation Language)。它是一種很像 Excel 公式的語法,專門用來在 DataTable 中計算新的欄。
想了解更多,可以參考 https://hazelnutparadise.github.io/insyra/#/CCL。
而如果你希望 Parquet 檔「不用整包讀進來」就先把資料篩掉,我們的parquet 包也支援 CCL。
FilterWithCCL:回傳篩選結果(不改原檔)
ctx := context.Background()
filtered, err := parquet.FilterWithCCL(ctx, "data.parquet", "(['訂單金額'] > 1000) && (['狀態'] == 'active')")
if err != nil {
panic(err)
}
filtered.Show()
ApplyCCL:直接改檔(會覆寫原檔)
ctx := context.Background()
err := parquet.ApplyCCL(ctx, "data.parquet", `
NEW('折扣後金額') = ['訂單金額'] * 0.9
NEW('是否大單') = ['訂單金額'] >= 5000
`)
if err != nil {
panic(err)
}
小提醒:Parquet 每一欄必須維持同一種型別。
所以在使用 CCL 新增欄位或運算時,請確保同一欄不要混到字串/數字/布林,不然可能會被自動轉型或直接出錯。
本篇範例使用的 Insyra 版本:v0.2.11
Insyra 官網:https://insyra.hazelnut-paradise.com
Insyra 說明文件:https://hazelnutparadise.github.io/insyra
Insyra GitHub 儲存庫:https://github.com/HazelnutParadise/insyra