🐾

Parquet形式はなぜヘッダーではなくフッターにメタデータを記載しているのか

に公開

はじめに

列指向型のデータ保持形式である Parquet 形式 (パーケット形式)について学んでいる中で、「なぜメタデータがファイルのヘッダーではなくフッターに書かれているのか」という点に疑問を持ちました。本記事では、その仕組みと理由を整理して紹介します。

内容は以下の流れで説明します。

  • Parquet形式とは
  • Parquet形式とCSV、RDBMSの比較
  • CSVをParquetに変換する
  • Parquetファイルの中身を確認する
  • なぜフッターにメタデータがあるのか

利用した製品

Parquet形式とは

Parquetは以下のような構造でデータをバイナリ形式で格納します。

  • ヘッダー
    • 固定文字列「PAR1」
  • 行グループ0
    • 列チャンクA
    • 列チャンクB
  • 行グループ1
  • 行グループN
  • フッター
    • バージョン
    • スキーマ情報
    • 補足情報
    • 各行グループと列チャンクのメタデータ(統計情報やオフセット位置など)
    • フッターのサイズ
    • 固定文字列「PAR1」

構造のポイントは以下の通りです。

  1. ヘッダーには「PAR1」という固定文字列が32バイトで記録されます。
  2. データは行単位でまとまりに分割されます。これが 行グループ です。(列指向といいつつまずはある程度の塊で行ごとに分割します)
  3. 行グループ内のデータは、列ごとに圧縮されたバイナリ形式で格納されます。これを 列チャンク と呼びます。
  4. フッターには、Parquetのバージョン、スキーマ情報、行グループや列チャンクごとの統計情報(最小値・最大値・NULL数など)、さらに各データのファイル内オフセット位置が格納されます。
  5. 最後にフッターのサイズと「PAR1」が再度記録されます。

Parquet形式とCSV、RDBMSの比較

CSVファイルとの違いをまとめてみました。

比較 CSV Parquet
構成 行指向 列指向
配置場所 1ファイル=1テーブル 1ファイル=1テーブル
データ格納方法 1行=1レコード 行グループ単位で列方向に圧縮
圧縮効率が非常に良い※
インデックス なし フッターに行グループ/列チャンクのオフセットを格納
統計情報 なし フッターに行グループ・列チャンク単位の統計情報を格納

統計情報やインデックスのようなメタデータを保持している点で、Parquetは単純なCSVよりもRDBMSに近い性質を持つと感じたので、軸は異なりますが、類似点をまとめます。

類似点 RDBMS (例: OracleDB) Parquet
構成 行指向 列指向
配置場所 データブロック単位 1ファイル=1テーブル
データ格納方法 複数レコードをブロックに格納 行グループ単位で列方向に圧縮
圧縮効率が非常に良い※
インデックス 別ブロックに格納 フッターに行グループ/列チャンクのオフセットを格納
統計情報 テーブルごとのメタ情報 フッターに行グループ・列チャンク単位の統計情報を格納

※例えば、都道府県別に並べたりすると東京都が圧倒的に多くなるので、いくつかの列グループはすべて東京都になることが想定されます。その場合、すべて同じデータの繰り返しのため、圧縮効率としては非常に高くなります。

CSVをParquetに変換する

今回は、1万人分の5教科の成績を乱数で生成したCSVを利用しました。
成績順に並べ替えて保存しておくことで、類似データが同じ行グループにまとまり、圧縮効率が向上します。

DuckDBをインストール

Parquet形式で出力できるDBはApache Hive、Google BigQuery、Amazon Redshiftなどありますが、
クライアント側で軽量に稼働する列指向DB「DuckDB」を用いると、簡単にParquet形式でデータを入出力できます。
Windowsのパッケージマネージャのchocolateyでインストールします。

$ choco install -y duckdb

CSVをParquetに変換

DuckDBコンソールでCSVをテーブルにインポートし、Parquet形式に変換します。
行グループのサイズは最小の2048に設定しました(デフォルトは約128MB)。

$ duckdb
D CREATE TABLE my_table AS SELECT * FROM read_csv_auto('C:\temp\input.csv');
D COPY my_table TO 'C:\temp\output.parquet' (FORMAT 'parquet', ROW_GROUP_SIZE 2048);
D .quit

出力結果のファイルサイズは以下の通りで、Parquetの方が小さくなっています。

$ dir
ディレクトリ: C:\temp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2025/09/24      8:47         255502 input.csv
-a----        2025/09/24      8:49         116462 output.parquet

Parquetファイルの中身を確認する

Parquetはバイナリ形式のためテキストエディタでは読めませんが、DuckDBを使えば直接参照できます。

データの確認

DuckDBでparquetファイルの中身を見てみます。
普通のSQLのようにテーブル名のところにファイルパスを書くだけで外部ファイルを読み込めます。

$ duckdb
D select * from 'C:\temp\output.parquet';
┌─────────┬──────────┬─────────┬───────┬─────────┬────────┬───────┐
│   ID    │ Japanese │ English │ Math  │ Science │ Social │ Total │
│ varchar │  int64   │  int64  │ int64 │  int64  │ int64  │ int64 │
├─────────┼──────────┼─────────┼───────┼─────────┼────────┼───────┤
│ 004491  │       89 │      90 │    97 │      93 │     88 │   457 │
│ 005143  │       89 │      87 │    97 │      93 │     91 │   457 │
│ 009238  │       80 │      96 │    86 │      96 │     94 │   452 │
│ 005437  │       99 │      98 │    90 │      98 │     65 │   450 │
│ 007022  │       96 │      82 │    90 │      96 │     84 │   448 │
│ 004331  │       75 │      90 │    88 │      91 │     95 │   439 │
│ 009226  │       95 │      58 │    99 │      87 │     99 │   438 │
│ 001993  │       97 │      63 │   100 │     100 │     73 │   433 │
│ 002020  │       95 │      91 │    69 │      97 │     81 │   433 │
│ 007364  │       65 │      93 │    98 │      99 │     78 │   433 │
│ 003443  │       51 │      96 │    95 │     100 │     89 │   431 │
│ 006638  │       90 │      96 │    84 │      94 │     67 │   431 │
│ 007817  │       81 │      53 │    99 │      98 │    100 │   431 │
│ 009325  │       62 │      88 │    95 │      93 │     93 │   431 │
│ 005954  │       64 │      94 │    74 │      98 │    100 │   430 │
│ 000551  │       94 │      90 │    90 │      61 │     94 │   429 │
│ 003826  │       87 │      78 │    85 │      80 │     98 │   428 │
│ 006216  │       92 │      94 │    75 │      74 │     93 │   428 │
│ 009474  │       77 │      80 │    79 │      93 │     98 │   427 │
│ 007316  │       80 │      95 │   100 │      76 │     74 │   425 │
│   ・    │        ・│        ・│     ・│       ・│      ・│     ・│
│   ・    │        ・│        ・│     ・│       ・│      ・│     ・│
│   ・    │        ・│        ・│     ・│       ・│      ・│     ・│
│ 007143  │       12 │      17 │    18 │      11 │     14 │    72 │
│ 008462  │        1 │       7 │    32 │       9 │     23 │    72 │
│ 009050  │       20 │      38 │     2 │       2 │     10 │    72 │
│ 009465  │        3 │      18 │    12 │       0 │     39 │    72 │
│ 002418  │       14 │      10 │    30 │      16 │      0 │    70 │
│ 006256  │       28 │       1 │    17 │      19 │      5 │    70 │
│ 006997  │        3 │       2 │    14 │      11 │     34 │    64 │
│ 000967  │        2 │       8 │    22 │      19 │     12 │    63 │
│ 005364  │       15 │       1 │    26 │      13 │      5 │    60 │
│ 000497  │       12 │       5 │     8 │       8 │     26 │    59 │
│ 001414  │        3 │       2 │     2 │      25 │     27 │    59 │
│ 006927  │        0 │       1 │    29 │      12 │     15 │    57 │
│ 008480  │       13 │      25 │     1 │       8 │     10 │    57 │
│ 003237  │        1 │      23 │     4 │      20 │      7 │    55 │
│ 004831  │        2 │      28 │    11 │       0 │     14 │    55 │
│ 000312  │       17 │       9 │    10 │      15 │      2 │    53 │
│ 005307  │       12 │      14 │     9 │      15 │      2 │    52 │
│ 003722  │       13 │       1 │    17 │      20 │      0 │    51 │
│ 003800  │        8 │      22 │     0 │       7 │     13 │    50 │
│ 002601  │        6 │       6 │     9 │       1 │     14 │    36 │
├─────────┴──────────┴─────────┴───────┴─────────┴────────┴───────┤
│ 10000 rows (40 shown)                              7 columns │
└─────────────────────────────────────────────────────────────────┘
D .quit

フッター情報の確認

フッターにあるメタデータを確認したい場合、DuckDBではテーブル名にparquet_metadata(ファイルパス)を指定するとメタデータだけを確認できます。

$ duckdb
D select row_group_id, path_in_schema, compression, row_group_num_rows, data_page_offset, stats_min,stats_max,stats_null_count from parquet_metadata('C:\temp\output.parquet');
┌──────────────┬────────────────┬─────────────┬────────────────────┬──────────────────┬───────────┬───────────┬──────────────────┐
│ row_group_id │ path_in_schema │ compression │ row_group_num_rows │ data_page_offset │ stats_min │ stats_max │ stats_null_count │
│    int64     │    varchar     │   varchar   │       int64        │      int64       │  varchar  │  varchar  │      int64       │
├──────────────┼────────────────┼─────────────┼────────────────────┼──────────────────┼───────────┼───────────┼──────────────────┤
│            0 │ ID             │ SNAPPY      │               2048 │                4 │ 000004    │ 009998    │                0 │
│            0 │ Japanese       │ SNAPPY      │               2048 │            10530 │ 0         │ 100       │                0 │
│            0 │ English        │ SNAPPY      │               2048 │            12790 │ 0         │ 100       │                0 │
│            0 │ Math           │ SNAPPY      │               2048 │            15050 │ 0         │ 100       │                0 │
│            0 │ Science        │ SNAPPY      │               2048 │            17314 │ 0         │ 100       │                0 │
│            0 │ Social         │ SNAPPY      │               2048 │            19578 │ 0         │ 100       │                0 │
│            0 │ Total          │ SNAPPY      │               2048 │            21972 │ 305       │ 457       │                0 │
│            1 │ ID             │ SNAPPY      │               2048 │            22376 │ 000003    │ 009999    │                0 │
│            1 │ Japanese       │ SNAPPY      │               2048 │            32876 │ 0         │ 100       │                0 │
│            1 │ English        │ SNAPPY      │               2048 │            35140 │ 0         │ 100       │                0 │
│            1 │ Math           │ SNAPPY      │               2048 │            37404 │ 0         │ 100       │                0 │
│            1 │ Science        │ SNAPPY      │               2048 │            39668 │ 0         │ 100       │                0 │
│            1 │ Social         │ SNAPPY      │               2048 │            41932 │ 0         │ 100       │                0 │
│            1 │ Total          │ SNAPPY      │               2048 │            43953 │ 266       │ 305       │                0 │
│            2 │ ID             │ SNAPPY      │               2048 │            44067 │ 000002    │ 010000    │                0 │
│            2 │ Japanese       │ SNAPPY      │               2048 │            54563 │ 0         │ 100       │                0 │
│            2 │ English        │ SNAPPY      │               2048 │            56827 │ 0         │ 100       │                0 │
│            2 │ Math           │ SNAPPY      │               2048 │            59091 │ 0         │ 100       │                0 │
│            2 │ Science        │ SNAPPY      │               2048 │            61355 │ 0         │ 100       │                0 │
│            2 │ Social         │ SNAPPY      │               2048 │            63619 │ 0         │ 100       │                0 │
│            2 │ Total          │ SNAPPY      │               2048 │            65626 │ 231       │ 266       │                0 │
│            3 │ ID             │ SNAPPY      │               2048 │            65738 │ 000001    │ 009992    │                0 │
│            3 │ Japanese       │ SNAPPY      │               2048 │            76274 │ 0         │ 100       │                0 │
│            3 │ English        │ SNAPPY      │               2048 │            78538 │ 0         │ 100       │                0 │
│            3 │ Math           │ SNAPPY      │               2048 │            80802 │ 0         │ 100       │                0 │
│            3 │ Science        │ SNAPPY      │               2048 │            83068 │ 0         │ 100       │                0 │
│            3 │ Social         │ SNAPPY      │               2048 │            85332 │ 0         │ 100       │                0 │
│            3 │ Total          │ SNAPPY      │               2048 │            87361 │ 190       │ 231       │                0 │
│            4 │ ID             │ SNAPPY      │               1808 │            87480 │ 000005    │ 009997    │                0 │
│            4 │ Japanese       │ SNAPPY      │               1808 │            96832 │ 0         │ 100       │                0 │
│            4 │ English        │ SNAPPY      │               1808 │            99094 │ 0         │ 100       │                0 │
│            4 │ Math           │ SNAPPY      │               1808 │           101354 │ 0         │ 100       │                0 │
│            4 │ Science        │ SNAPPY      │               1808 │           103618 │ 0         │ 100       │                0 │
│            4 │ Social         │ SNAPPY      │               1808 │           105878 │ 0         │ 100       │                0 │
│            4 │ Total          │ SNAPPY      │               1808 │           108256 │ 36        │ 190       │                0 │
├──────────────┴────────────────┴─────────────┴────────────────────┴──────────────────┴───────────┴───────────┴──────────────────┤
│ 35 rows                                                                                                             8 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D .quit

この結果から、行グループや列チャンクごとの統計情報や圧縮方式、オフセット位置を確認できます。
例えば「全教科成績上位100名の数学の平均点を求めたい」といった場合を考えます。

この場合、行グループ「0」に属する「数学」の列チャンクに含まれる2048レコードを読み込んで、その中に成績上位100名の数学の点数が含まれるので、それ以外のデータは読む必要はありません。
こういったヘッダー情報を使って必要最低限に読み込み量を調整することで効率的に計算できます。

なぜフッターにメタデータがあるのか

通常、メタデータはヘッダーに配置されるイメージがあります。
ヘッダーにメタデータサイズを置いておけば、冒頭部分を読み込むだけでアクセス可能だからです。

一方、Parquetではメタデータがフッターにあります。調べたところ理由は 書き込みの順序 にあります。

Parquetのフッターには、行グループや列チャンクのオフセット位置や統計情報が含まれています。
これらはデータを書き込む時点ではまだ確定していないため、書き込みを完了した後でなければ正しい情報を記録できません。
そのため、データ本体をすべて書き込んだ後、最後にフッターとしてメタデータを追記する設計になっています。
また、非常に大きなサイズのデータを取り扱うこともあり、ストリーミングで通信しながら処理する必要がある場合にも役立ちます。

まとめ

Parquet形式は、行グループ/列チャンク単位の統計やオフセットをフッターに持つことで、効率的なスキップ読み込みを可能にしています。
集計や分析処理に限定すれば非常に高速にアクセスできるフォーマットです。

掲載したコマンド、データなどはサンプルになります。本コマンド、データを使用することで発生するいかなる損害や不利益について、当社は一切の責任を負いませんので自己の責任においてご利用ください。

参考記事

Discussion