Parquet 檔案格式
Apache Parquet,用於巨量資料分析的欄式儲存格式。
- MIME 類型
- application/vnd.apache.parquet
- 副檔名
- .parquet
- 分類
- 資料與資料庫
Magic number(檔案簽章)
| 0x00000000 | 50415231 | PAR1 | 'PAR1' header |
| -0x00000004 | 50415231 | PAR1 | 'PAR1' footer (last 4 bytes) |
關於 Parquet 格式
Parquet 是一欄一欄存資料,不是一列一列存,所以一個只碰到一百欄裡三欄的查詢就只讀那三欄;而且每一欄壓得更好,因為相鄰的值型別相同、範圍通常也很窄。所有描述性的東西——結構描述、row group、每個 column chunk 的位移、編解碼器與最小/最大統計值——都用 Thrift 的 compact protocol 序列化,並且寫在資料後面,所以這個檔案是頭尾都有錨點的:位移 0 的 PAR1 讓掃描工具認出格式,檔尾的 PAR1 則錨定讀取端真正的起點——footer。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 4 | File magic 'PAR1' | 4 個位元組 50 41 52 31。它存在的目的是讓掃描檔案開頭的工具能叫出格式名稱;沒有任何讀取端會從這一端解析其他東西。footer 有加密的檔案,這裡和檔尾放的是 PARE,等於告訴讀取端 footer 前面還有一段檔案層級的加密中繼資料。 |
| 4 | variable | First column chunk | 資料緊接在 magic 之後開始——一頁接一頁的編碼值,每一頁前面都有一個 Thrift compact 的 PageHeader。檔案前端沒有結構描述、沒有索引、也沒有列數,所以 Parquet 檔案的第一個 MB 幾乎不會告訴你裡面裝了什麼。 |
| variable | variable | FileMetaData (Thrift compact) | 結構描述、row group,以及每個 column chunk 的位移、編解碼器、編碼方式與統計值。它就緊貼在最後 8 個位元組前面,讀取端是用那個位置減掉 footer 長度來找它的起點——絕對不會從位移 0 往後掃。 |
| -8 | 4 | Footer length | 無號 32 位元小端序,是它前面那段 FileMetaData 的位元組數。讀取端會 seek 到最後 8 個位元組、檢查 magic、讀這個數字,然後往回 seek 那麼多位元組。這裡的值錯了就是致命的,即使每一頁資料都完好無缺。 |
| -4 | 4 | Trailing magic 'PAR1' | 和位移 0 相同的 4 個位元組——footer 有加密時是 PARE。因為這個格式的錨點在尾巴,這幾個位元組是每個讀取端第一個檢查的東西,所以下載到一半的 Parquet 檔案會被直接拒收,儘管它的標頭好得很。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| ORC | 同一批資料管線裡的另一種欄式格式,footer 放檔尾的設計也一樣。ORC 開頭是 3 個位元組 4F 52 43('ORC'),不是 4 位元組的 magic,而它的最後一個位元組根本不是 magic,而是 postscript 長度,一個小於 256 的單一數值。所以讀最後 4 個位元組:PAR1 是 Parquet,一個隨意的小位元組就是 ORC。 |
| Avro OCF | Avro 的物件容器檔案出自同一批寫入工作,但它是列式而且自我描述。標頭是 4 個位元組 4F 62 6A 01('Obj' 後接 1),接著一張放 avro.schema 與 avro.codec 的中繼資料表,然後是 16 位元組的同步標記。它沒有尾端 magic,所以 Avro 檔案可以一直往後追加還照樣讀得動,Parquet 不行。 |
| Encrypted Parquet (PARE) | 版面配置相同,只是頭尾的 magic 換成 PARE 而不是 PAR1,這是刻意的:讓舊的讀取端卡在 magic,而不是卡在讀不懂的 Thrift。footer 加密時,尾端 magic 前面那 4 個位元組的長度涵蓋了加密中繼資料加上加密後的 footer,所以同一張表的這個數字會比明文的情況大。 |
| SQLite 資料庫 | 兩者都會被當成「那個分析用的檔案」傳來傳去,也都是單一檔案,但 SQLite 是列式儲存、magic 在前 16 個位元組,而且完全沒有尾端標記。如果位移 0 是 PAR1、最後 4 個位元組也是 PAR1,中間那段就不用查了。 |
常見的壞法
- 尾巴被截斷。任何讓傳輸提早結束的事情——物件儲存的下載被砍掉、寫入端在 close 之前掛掉——都會留下開頭的 magic 和好幾 GB 完好的頁面,後面卻沒有寫成的 footer。每個引擎都會回報「這不是 Parquet 檔案」,因為它們檢查的是最後 4 個位元組,不是前 4 個。
- footer 長度對不上了。把兩個 Parquet 檔案串在一起、後面多加一個位元組,或讓編輯器補上一個換行,錨點就位移了:尾端 magic 可能照樣讀到 PAR1,前面那 4 個位元組卻指到某一頁的中間,於是 Thrift 解析器卡在垃圾資料上,而不是告訴你這個檔案被編輯過。
- 資料頁裡有一個位元組被翻掉。footer 解析得動、結構描述列得出來、那一欄一路讀到損壞的那一頁都正確,然後解壓縮器報錯、或是值解出來全是亂七八糟的東西。Parquet 的每頁 CRC 是選用的,所以很多寫入端根本沒留下任何線索指出是哪一頁出事。
- 同一個目錄下結構描述悄悄漂移。這不是單一檔案損壞,卻是大家真的會碰到的失敗:每個檔案單獨看都有效,偏偏其中一個把某欄的型別寫成 int32、其他都寫 int64,查詢引擎就把整份資料集拒收。答案在每個檔案的 footer 裡,不在資料裡。
這個格式現在還在用嗎
Parquet 是分析世界的預設儲存格式。Spark、Trino、DuckDB、Polars、pandas、BigQuery 匯出與 Snowflake 的 UNLOAD,全都讀寫它,而建構在它之上的物件儲存表格格式——Iceberg、Delta Lake、Hudi——本質上就是一堆 Parquet 檔案的目錄加一份中繼資料日誌。只要資料量大,它就取代了 CSV,因為 CSV 每次查詢都得從頭到尾重讀重剖,而 Parquet 讀取端是先查 footer、用統計值把不可能命中的 row group 直接丟掉,再只解壓縮被問到的那幾欄。它仍然不適合的場合是小資料和給人看的交換:兩百列的對照表用 CSV 好管理得多;而一筆一筆的串流寫入該交給 Avro,因為 Parquet 檔案要等到 footer 寫下去的那一刻才成為有效檔案。
在這個站上打開 Parquet 檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 Parquet 搞混的格式,可以比對它們的開頭位元組。
| 格式 | 開頭位元組 | 副檔名 |
|---|---|---|
| SQLite 資料庫 | 53 51 4C 69 74 65 | .sqlite |