200字
Go 語言如何操作 Parquet?2026 就用最潮的 Insyra 函式庫 6招解決巨量資料太巨量的問題!
2025-12-31
2025-12-31

資料分析時,我們常會使用 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(中繼資料),記錄欄位資訊、每個區塊在哪、統計資訊等,讓讀取端更容易「只抓需要的部分」。

這樣的好處很直覺:

  1. 只讀需要的欄位(少 I/O、少記憶體)

  2. 可以分段讀(例如只讀第 0、1、2 個 row group)

  3. 壓縮效果常常比 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

評論