Parquet ファイル形式
Apache Parquet。ビッグデータ分析向けの列指向ストレージ形式。
- MIME タイプ
- application/vnd.apache.parquet
- 拡張子
- .parquet
- カテゴリ
- データ・データベース
マジックナンバー(ファイル署名)
| 0x00000000 | 50415231 | PAR1 | 'PAR1' header |
| -0x00000004 | 50415231 | PAR1 | 'PAR1' footer (last 4 bytes) |
Parquet 形式について
Parquet はテーブルを行単位ではなく列単位で格納します。100 列のうち 3 列しか触らないクエリはその 3 列しか読みませんし、隣り合う値が同じ型で値域も狭いことが多いため、列ごとの圧縮がよく効きます。説明的なもの(スキーマ、行グループ、各カラムチャンクのオフセット・コーデック・最小最大統計)は Thrift の compact protocol でシリアライズされ、データの後ろに書かれます。ファイルが両端で錨を打たれているのはこのためで、オフセット 0 の PAR1 は走査するツールに形式を告げ、末尾の PAR1 は読み手が実際に読み始めるフッターの位置を固定します。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 4 | File magic 'PAR1' | 4 バイトの 50 41 52 31。ファイルの先頭を走査するツールが形式を名乗れるようにするためだけに存在し、こちら側から他の何かをパースする読み手はいません。フッターを暗号化して書かれたファイルは、ここと末尾の両方が PARE になり、フッターの前に暗号メタデータが来ることを読み手に伝えます。 |
| 4 | variable | First column chunk | データはマジックナンバーの直後から始まります。符号化された値のページが並び、それぞれ Thrift compact の PageHeader で始まります。先頭にはスキーマもインデックスも行数もないので、Parquet ファイルの最初の 1 MB を見ても中身についてはほとんど何も分かりません。 |
| variable | variable | FileMetaData (Thrift compact) | スキーマ、行グループ、各カラムチャンクのオフセット・コーデック・エンコーディング・統計。最後の 8 バイトのすぐ前に位置し、読み手はその位置からフッター長を引いて先頭を求めます。オフセット 0 から前方に走査することは決してありません。 |
| -8 | 4 | Footer length | 直前にある FileMetaData のバイト数を、符号なし 32 ビットのリトルエンディアンで。読み手は末尾 8 バイトへシークしてマジックナンバーを確認し、この数値を読み、その分だけ戻ります。ここが間違っていると、データページがすべて無傷でも致命的です。 |
| -4 | 4 | Trailing magic 'PAR1' | オフセット 0 と同じ 4 バイト(フッターが暗号化されていれば PARE)。形式が末尾で錨を打っている以上、どの読み手もまずこのバイト列を確かめます。ヘッダが完璧でも、半分しかダウンロードできていない Parquet が門前払いされるのはそのためです。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| ORC | 同じパイプラインに出てくるもう 1 つの列指向形式で、フッターを末尾に置く設計も同じです。ORC は 4 バイトのマジックナンバーではなく 3 バイトの 4F 52 43('ORC')で始まり、最後のバイトはマジックナンバーですらなく postscript の長さ、つまり 256 未満の値が 1 つ入っているだけです。だから末尾 4 バイトを読めば判定できます。PAR1 なら Parquet、意味のない小さな 1 バイトなら ORC です。 |
| Avro OCF | Avro のオブジェクトコンテナファイルは同じ取り込みジョブから出てきますが、行指向で自己記述的です。ヘッダは 4 バイトの 4F 62 6A 01('Obj' と 1)、続いて avro.schema と avro.codec を持つメタデータのマップ、そして 16 バイトの同期マーカー。末尾のマジックナンバーが無いので、Avro のファイルは追記しても読めますが、Parquet はそうはいきません。 |
| Encrypted Parquet (PARE) | レイアウトは同じで、両端のマジックナンバーだけが違います。PAR1 ではなく PARE。古い読み手が読めない Thrift ではなくマジックナンバーの段階で失敗するよう、意図的にそうしてあります。フッターが暗号化されている場合、末尾マジックナンバーの前の 4 バイトの長さは暗号メタデータと暗号化されたフッターの両方を含むので、同じテーブルでも平文のときより大きな値になります。 |
| SQLite データベース | どちらも「分析用のファイル」として渡されますし、どちらも単一ファイルですが、SQLite は行ストアで、マジックナンバーは先頭 16 バイトにあり、末尾のマーカーは一切ありません。オフセット 0 が PAR1 で末尾 4 バイトも PAR1 なら、途中を確認する必要はありません。 |
よくある壊れ方
- 末尾の欠落。転送を途中で止めるもの(オブジェクトストレージからのダウンロードの中断、close 前に落ちた書き込みプロセス)は、先頭のマジックナンバーと無傷のページを何 GB も残したまま、フッターだけを書かずに終わります。どのエンジンも「これは Parquet ファイルではない」と報告します。判定しているのが先頭 4 バイトではなく末尾 4 バイトだからです。
- フッター長が合わなくなっている。Parquet ファイルを 2 つ連結する、1 バイト追記する、エディタが末尾に改行を足す。どれも錨をずらします。末尾のマジックナンバーは PAR1 のままなのに、その前の 4 バイトがページの途中を指すことになり、Thrift のパーサーは「編集された」ではなくゴミを読んだというエラーで落ちます。
- データページ内の 1 バイト反転。フッターはパースでき、スキーマも列挙でき、壊れたページの直前までは列も正しく読めて、そこで展開器がエラーを出すか値がでたらめにデコードされます。ページごとの CRC は Parquet では省略可能なので、どのページがずれたかを指し示す手掛かりを残さない書き手も多いです。
- ディレクトリ全体で静かに起きるスキーマのずれ。1 つのファイルの破損ではなく、現実に人がぶつかるほうの障害です。個々のファイルはそれぞれ正当なのに、そのうち 1 つだけある列を int32 として型付けし、他は int64 と言っている。するとクエリエンジンはデータセット全体を拒否します。答えはデータではなく、各ファイルのフッターの中にあります。
この形式の現在地
Parquet は分析の世界における既定のストレージ形式です。Spark、Trino、DuckDB、Polars、pandas、BigQuery のエクスポート、Snowflake のアンロード、どれも読み書きしますし、オブジェクトストレージ上のテーブル形式(Iceberg、Delta Lake、Hudi)は要するに Parquet ファイルのディレクトリとメタデータログの組み合わせです。大きいデータでは CSV を置き換えました。CSV はクエリのたびに端から端まで読み直してパースし直す必要がありますが、Parquet の読み手はフッターを見て、統計から除外できる行グループを捨て、要求された列だけを展開します。今も向いていないのは小さなデータと人が扱う受け渡しです。200 行の対応表は CSV のまま持つほうが楽ですし、1 レコードずつのストリーミング取り込みは Avro の領分です。Parquet のファイルはフッターを書き終えて初めて有効になるからです。
Parquet ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
Parquet と混同しやすい形式です。先頭バイトを見比べてください。
| 形式 | 先頭バイト | 拡張子 |
|---|---|---|
| SQLite データベース | 53 51 4C 69 74 65 | .sqlite |