列指向のファイルフォーマットはなぜ高速なのか?
ファイルフォーマット
データレイクにおいて列指向のファイルフォーマット (Parquet) が高速であるため用いられると聞いたので、その理由を調べた。
行指向
行指向フォーマットでは、データを行単位で連続して格納する。
[ID:1, Name:"Taro", Age:25, Salary:3,000,000]
[ID:2, Name:"Hanako", Age:30, Salary:4,000,000]
[ID:3, Name:"Jiro", Age:30, Salary:4,000,000]
...
行指向のファイルフォーマットの代表例は以下。
- CSV
- JSON
- Avro
列指向
列指向フォーマットでは、データを列単位で連続して格納する。
[ID: 1, 2, 3, ...]
[Name: "Taro", "Hanako", "Jiro", ...]
[Age: 25, 30, 28, ...]
[Salary: 3,000,000, 4,000,000, 3,000,000, ...]
列指向のファイルフォーマットの代表例は以下。
- Parquet
- ORC
なぜ列指向が高速なのか?
重要な点は 各列においてデータ型が統一されている ということ。
必要な列だけ読み込める
例えば「全従業員の平均年齢を計算する」というクエリの場合
- CSV(行指向): すべての列(ID、Name、Age、Salary)を読み込む必要がある
- Parquet(列指向): Age 列だけを読み込めばよい
100 列あるテーブルで 1 列だけ必要な場合、理論上は読み込むデータ量が 1 / 100。
圧縮効率がよい
同じデータ型が連続して並ぶため、圧縮が効果的。
年齢列: [25, 25, 25, 26, 26, 27, ...] → 同じ値が連続するため高圧縮
文字列列: ["営業部", "営業部", "技術部", ...] → 辞書エンコーディングで効率化
CSV では各行に異なる型のデータが混在するため、このような最適化ができない。
CPU に最適化されている
同じ型のデータが連続して配置されているため、CPU の処理において有利。
- CPU のベクトル演算 (SIMD) を活用できる
- メモリアクセスパターンが予測しやすい
- キャッシュヒット率が向上
では全部列指向のフォーマットにすればよいのでは?
列指向フォーマットには重要なトレードオフがあり、すべてのユースケースに適しているわけではない。
行単位のアクセスが遅い
ユーザー情報を 1 件取得する場合
- CSV(行指向):
- 1 行読めば完了
- Parquet(列指向):
- ID 列から該当行を探す
- Name 列から該当位置を読む
- Age 列から該当位置を読む
- Salary 列から該当位置を読む
Webアプリケーションで「ユーザーID = 123の情報を表示」のような操作は頻繁するが、列指向では非効率。
更新が複雑
データを追加する場合
- CSV(行指向):
- 新しい行を末尾に追加するだけ
- Parquet(列指向):
- すべての列ファイルを開く
- 各列に値を追加
さらに 1 行だけ更新する場合、列指向では複数の列ファイルを書き換える必要があり、トランザクション処理が複雑。
小データで非効率
列指向フォーマットは
- 各列ごとにメタデータが必要
- 圧縮・エンコーディングの情報
- 統計情報(最小値、最大値など)
数百行程度の小さなデータでは、このメタデータのオーバーヘッドで逆に非効率。
実装が複雑
トランザクションシステム(ECサイト、銀行システムなど)では
- 注文を即座に記録したい
- 在庫を即座に更新したい
- 1 件ずつの読み書きが主体
列指向の複雑な構造は、このような用途には向かない。
よくあるアーキテクチャ
行指向と列指向ではトレードオフがあるため、実システムでは 2 つを用いたハイブリット構成が主流。
エンコーディングについて
Parquet は自動的に各列をエンコーディングしてくれるので、効率がよくなる。
- 各列のカーディナリティ(ユニーク値の数)を分析
- カーディナリティが低い列に自動で辞書エンコーディング適用
- データ型に応じて最適なエンコーディングを選択
例えば以下の通り。
- 部署名(10種類程度) → 辞書エンコーディング
- メールアドレス(ほぼ全てユニーク) → 辞書化しない
- 日付(限られた範囲) → Delta encoding
- 金額(連続値) → Bit-packing
辞書エンコーディング
文字列を圧縮する方法。
営業部
営業部
営業部
人事部
技術部
... (以下繰り返し)
**辞書エンコーディング適用後**
【辞書】
0 → "営業部"
1 → "技術部"
2 → "人事部"
【実データ】
0, 0, 0, 2, 1, ...
【さらにランレングス符号化】
0*3, 2, 1, ...
**圧縮前**
- "営業部" = 9 バイト(UTF-8 で 3 文字 × 3 バイト)
- 100 万行 × 9 バイト = 9 MB
**圧縮後**
- 辞書: 3 種類 × 9 バイト = 27 バイト
- ID 配列: 100 万行 × 1 バイト* = 1 MB
- 合計 ≈ 1 MB (約 9 分の 1 に圧縮)
文字列比較より数値比較のほうが高速、数値のほうがメモリ消費量も下がる、などの利点も。
Delta Encoding
日付を圧縮する方法。
日付は連続的に増加することが多い:
【元データ】
2024-01-01 (エポック秒: 1704067200)
2024-01-02 (エポック秒: 1704153600)
2024-01-03 (エポック秒: 1704240000)
【Delta Encoding】
基準値: 1704067200
差分: 0, +86400, +86400, +86400, ...
【さらに圧縮】
差分が一定 (86400) なのでランレングス符号化
→ "86400*3"
数GB → 数百バイトになることも。
Bit-packing
整数値を圧縮する方法。
基準値: 100
差分: 0, 5, 10, 15, 20 (各 5 bit)
32 bit × 5 個 = 160 bit
↓
32bit(基準値) + 5 bit × 5 個 = 57 bit
→ 約 64 % 圧縮
感想
データやワークロードの特性をうまく利用したファイルフォーマットであることがわかった。特にエンコーディングは賢いなと思った。
Discussion