Skip to content

✍️ 필사 모드: Columnar ストレージ完全ガイド 2025: Parquet, ORC, Apache Arrow, Dremel — 分析 DB が 10,000 倍速い理由

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに: 同じデータ、1000 倍の差

一つの実験

1 億行の販売データ。各行は (id, timestamp, country, product, category, price, ...) で計 50 カラム。

質問: 「韓国で売れた商品の平均価格は?」

SELECT AVG(price) FROM sales WHERE country = 'KR';

CSV や MySQL のような row-oriented ストレージ:

  1. 1 億行をすべて読み込む (50 カラム全部)。
  2. country でフィルタ。
  3. price の平均を計算。
  4. ディスク I/O: 数十 GB。数分かかる。

同じデータが Parquet なら:

  1. country カラムだけ読んでフィルタ。
  2. price カラムだけ読んで平均計算。
  3. ディスク I/O: 数百 MB。数秒。

100~1000 倍の差はよくある。Snowflake, BigQuery, Spark が TB 単位のデータを数秒でクエリするのはこのため。

なぜ今 columnar が標準か

2000 年代初頭まで DBMS はほぼすべて row-oriented だった。2005 年 MIT の Michael Stonebraker 教授の C-Store 論文が columnar DB の性能優位を示し、その後 Vertica (2005)、Apache Parquet (2013)、Apache ORC (2013)、Apache Arrow (2016)、ClickHouse、Snowflake が登場。いまや OLAP ワークロードの事実上の標準。


1. Row-oriented vs Column-oriented

Row ストレージ

従来の DB (PostgreSQL, MySQL) は各行を連続したバイトとして保存:

Row 1: [id=1][time=T1][country=KR][product=A][price=100]
Row 2: [id=2][time=T2][country=US][product=B][price=200]
Row 3: [id=3][time=T3][country=KR][product=C][price=150]

長所: 1 行全体を高速に読める、トランザクション自然、OLTP 最適。 短所: 特定カラムだけ必要でも全行を読む、I/O 無駄、圧縮効率低い。

Column ストレージ

同じカラムの値を連続したブロックに保存:

id column:      [1, 2, 3, 4, 5, ...]
country column: [KR, US, KR, JP, KR, ...]
price column:   [100, 200, 150, 300, 120, ...]

長所: 必要なカラムだけ読める、同じ型が連続で圧縮が効く、CPU キャッシュに優しい、OLAP 最適。 短所: 1 行再構成に複数カラムアクセス必要、insert/update が遅い、トランザクション複雑。

OLTP vs OLAP

項目Row-oriented (OLTP)Column-oriented (OLAP)
MySQL, PostgreSQLParquet, ClickHouse
クエリ個別レコード集計・フィルタ・スキャン
書き込み小さく頻繁バルクロード
読み込みSELECT * FROM users WHERE id=...SELECT AVG(price) FROM sales WHERE ...
圧縮率低 (2:1)高 (5:1 ~ 20:1)
インデックス重要カラム統計で代替

2. Parquet: Dremel の影響

誕生

Apache Parquet は 2013 年 Twitter と Cloudera が共同開発。Google の Dremel 論文 (2010) の影響を受け、ネストデータを効率的に格納する構造を採用。Spark, Hive, Presto, Trino, Snowflake, BigQuery, Athena, Impala すべてが対応。

ファイル構造

File
 ├── Magic "PAR1"
 ├── Row Group 1
 │    ├── Column Chunk 1 (Data Pages)
 │    ├── Column Chunk 2
 │    └── Column Chunk 3
 ├── Row Group 2 ...
 ├── Footer (schema, offsets, stats)
 └── Magic "PAR1"

階層:

  • Row Group: 数百 MB ~ 1 GB の論理ブロック。並列処理単位。
  • Column Chunk: Row Group 内の 1 カラム全体。
  • Data Page: 最小単位 (デフォルト 1 MB)。圧縮/エンコーディング単位。

メタデータの力

Parquet の footer には schema、row group オフセット、カラム統計 (min/max/null count)、任意の Bloom filter、page index が格納される。これで row group 全体をスキップできる。

Predicate Pushdown

SELECT * FROM sales WHERE price > 1000;

エンジンは各 row group の price min/max を確認。max <= 1000 の row group はスキップ。適切に整列されていれば I/O の 99% を削減可能。

Bloom Filter

Parquet 1.11+ で任意で格納可能。user_id = 12345 のような点検索で min/max が効かない場合に有効。特にランダムな ID/UUID に効果的。

Page Index

Parquet 2.9+ はページ単位の min/max を別途保持。クラウドストレージ (S3) で必要なバイト範囲だけ fetch できる。


3. Dremel アルゴリズム: ネストデータの Columnar 表現

課題

Columnar はフラットなデータに向くが、JSON や Protocol Buffer のようなネストは?

{
  "id": 1,
  "name": "Alice",
  "addresses": [
    {"city": "Seoul", "zip": "12345"},
    {"city": "Busan", "zip": "67890"}
  ]
}

Repetition Level と Definition Level

Dremel の解答: 各値に 2 つの整数を追加。

  • Repetition level (R): その値が「反復の」どの深さにいるか。
  • Definition level (D): 値が「定義された」最大の深さ。

addresses.city の例:

ValueRD
"Seoul"02
"Busan"12
"LA"02
NULL01

この 2 つの数値で元の構造を損失なく復元できる (数学的に証明済み)。これにより Parquet は:

  • ネストデータを columnar で格納。
  • Schema evolution に対応。
  • NULL を効率的に処理。
  • addresses.city だけを読むといった部分選択が可能。

4. 圧縮とエンコーディング

Encoding と圧縮

Parquet は encoding が先、圧縮が後

Plain Encoding

値をそのまま保存。他が不利な場合のフォールバック。

Dictionary Encoding

カーディナリティが低いカラムで圧倒的。

原本:       [KR, US, KR, JP, KR, KR, US, JP, ...]
Dictionary: [KR=0, US=1, JP=2]
Values:     [0, 1, 0, 2, 0, 0, 1, 2, ...]

文字列を小さな整数に置き換え、さらに bit-packing。country カラムで 1000 倍以上のサイズ削減が普通。辞書が一定サイズを超えると plain に自動フォールバック。

Run-Length Encoding (RLE)

原本: [1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3]
RLE:  [(1,4), (2,2), (3,5)]

Parquet は「RLE/Bit-Packing Hybrid」として Dictionary Encoding と組み合わせる。

Bit Packing

整数が小さい範囲にあれば必要ビットだけ使用。0-3 の 32 ビット整数を 2 ビットに詰めると 16 倍圧縮。

Delta Encoding

原本:  [100, 102, 105, 108, 110, 115]
Delta: [100, 2, 3, 3, 2, 5]

単調増加するタイムスタンプや sequence ID に最適。Parquet の DELTA_BINARY_PACKED

Byte Stream Split

IEEE 754 float で各バイト位置ごとにストリームを分離し、一般圧縮が効くようにする。科学データに有効。

圧縮アルゴリズム

アルゴリズム圧縮率速度用途
UNCOMPRESSED1x最速ベンチ
SNAPPY2-3xとても速いバランス (デフォルト)
LZ42-3xとても速い復号が速い
GZIP4-5x遅い高圧縮
BROTLI4-5xWeb 向け
ZSTD4-5x速い2025 推奨
df.write.option("compression", "zstd").parquet("data.parquet")

組み合わせ効果

1 億行の e-commerce データ:

フォーマットサイズクエリ時間
CSV12 GB180 s
JSON28 GB300 s
Parquet (uncompressed)6 GB15 s
Parquet (snappy)2 GB8 s
Parquet (zstd)1.2 GB7 s

5. ORC: Hive のパートナー

Apache ORC (Optimized Row Columnar) は 2013 年 Hortonworks が Hive 向けに開発。概念は Parquet と近いが用語が異なる。

  • Stripe: Parquet の row group 相当 (デフォルト 250 MB)。
  • Stream: Stripe 内のカラムデータ。
  • Footer: ファイルメタデータ。

ORC vs Parquet

項目ParquetORC
エコシステムSpark, Trino, Flink, PandasHive, Presto
ネストデータDremel (強力)Flatten 中心
統計Row group 単位Stripe + stride (細かい)
ACID限定的Hive transactional tables
圧縮率若干高い

目安: Spark/Python/汎用 → Parquet。Hive 中心 DWH → ORC。

Stride Level Statistics

ORC は stripe 内の stride (デフォルト 10,000 行) 単位で統計を保持し、より細かいフィルタリングが可能。ただし Parquet の page index 登場で差は縮まった。

Hive ACID

ORC は Hive 0.14+ で ACID をサポート。base + delta file と定期 compaction。


6. Apache Arrow: メモリの Columnar

別の問題

Parquet/ORC はディスクフォーマット。Arrow はメモリフォーマット。Python pandas → Spark JVM → Java API と渡すたびに serialization/deserialization が発生し、クエリより長くなることもある。

誕生

2016 年、Wes McKinney (pandas 作者) らが発表。目標:

「言語・システムを横断する標準のメモリ Columnar フォーマット」

特徴

  • Zero-copy 読み取り: serialization 不要。
  • 言語中立: C++, Java, Python, R, Rust, Go などに対応。
  • SIMD 親和: ベクトル化演算を直接実行可能。
  • Interoperability: Arrow 配列をシステム間で即共有。

メモリレイアウト

Int32 配列: [1, 2, 3, NULL, 5]

Validity bitmap: [1, 1, 1, 0, 1]
Values buffer:   [1, 2, 3, 0, 5]

各カラムは validity bitmap、values buffer、(可変長の場合) offsets buffer。CPU キャッシュと SIMD に最適化。

Zero-Copy の威力

spark.conf.set("spark.sql.execution.arrow.pyspark.enabled", "true")
df.toPandas()

Pickle ベースより 10 倍以上高速

Arrow Flight

gRPC ベースで Arrow データをネットワーク経由で交換。ODBC/JDBC より 20 倍以上速い

DataFusion

Arrow プロジェクト内の Rust 製 SQL エンジン。Polars や InfluxDB IOx が内部で使用。

DuckDB

import duckdb
import pyarrow.parquet as pq

arrow_table = pq.read_table("data.parquet")
result = duckdb.query("SELECT country, AVG(price) FROM arrow_table GROUP BY country").to_df()

ディスクに書かずメモリ上でクエリ。ローカル分析の革命。


7. ベクトル化実行

従来の Tuple-at-a-time (Volcano)

while ((tuple = child->next()) != NULL) {
    if (predicate(tuple)) emit(tuple);
}

関数呼び出しオーバーヘッド、CPU パイプライン活用不可、分岐予測失敗、キャッシュミス多発。

ベクトル化 (Batch-at-a-time)

int32_t prices[1024];
for (int i = 0; i < 1024; i += 8) {
    __m256i v = _mm256_loadu_si256((__m256i*)&prices[i]);
    __m256i threshold = _mm256_set1_epi32(1000);
    __m256i result = _mm256_cmpgt_epi32(v, threshold);
}

SIMD で 1 命令 8~16 値処理。一般的に 2-10 倍高速

Snowflake, ClickHouse (1024-65536 行バッチ)、DuckDB (1024 行 + LLVM JIT)、Spark SQL (whole-stage codegen) がこれを採用。TU Munich の HyPer/Umbra はさらに進んで LLVM IR にコンパイルして実行。


8. パーティショニングと Z-Ordering

パーティショニング

sales/
├── year=2023/
│   ├── month=01/
│   └── ...
├── year=2024/
└── year=2025/

WHERE year=2024 AND month=03 でそのフォルダだけ読む。

Hive-style

s3://bucket/sales/year=2025/month=04/day=15/

ディレクトリ名がそのままパーティションカラム。ファイルには保存されない。

パーティション爆発

分単位などの過度な細分化は数百万個の小さなファイルを生み、list API レイテンシやメタデータ負担を引き起こす。目安: 各パーティション最低数百 MB。

Z-Ordering

Z-order (Morton code) は複数カラムのビットをインターリーブして 1 次元順序に。複数カラムの min/max が同時に効く。Delta Lake/Iceberg の OPTIMIZE ... ZORDER BY (country, product) がこれを実装。


9. Delta Lake / Iceberg / Hudi

Parquet の限界

Parquet はファイルフォーマットに過ぎず、ACID なし、schema evolution 手動、time travel なし、削除/更新が複雑。

Lakehouse フォーマット

  • Delta Lake (Databricks): Parquet + JSON transaction log、ACID、time travel、Z-ordering。
  • Apache Iceberg (Netflix/Apple): Parquet/ORC + manifest file、hidden partitioning、snapshot isolation。
  • Apache Hudi (Uber): Parquet + timeline、upsert/delete 最適化。

2024 年以降 Iceberg が業界標準に収束 (AWS, Snowflake, Databricks すべて対応)。

Hidden Partitioning

Iceberg はテーブル定義時にパーティション変換を宣言。ユーザーは論理カラムでクエリし、Iceberg が自動で month(ts) を計算。


10. 実践 Tips と落とし穴

Tip 1: Row group サイズ

df.write.option("parquet.block.size", 134217728).parquet("out")  # 128MB

128 MB - 512 MB が sweet spot。

Tip 2: カラム順

Parquet は書き込み順で保存。よく使うカラムを先頭に。

Tip 3: Dictionary 上限

df.write.option("parquet.dictionary.page.size", 2097152).parquet("out")

Tip 4: 圧縮

df.write.option("compression", "zstd").parquet("out")

2025 年は ZSTD がほぼ常に正解。

Tip 5: ファイルサイズ

Sweet spot 256 MB - 1 GB。Spark では coalesce()repartition() で調整。

落とし穴 1: Small Files

多数の小さなファイルは災害。list オーバーヘッド、メタデータ負担、schema 不整合リスク。Delta/Iceberg の OPTIMIZE や定期 compaction で解消。

落とし穴 2: Schema Evolution

Parquet 自体は schema 固定。Delta/Iceberg を使うか Spark の mergeSchema

落とし穴 3: Decimal と Timestamp

INT96 timestamp は deprecated → INT64。Decimal の表現はバージョンで変わる。システム間の互換性確認必須。

落とし穴 4: パーティション数爆発

月次・日次まで。それ以下はカラム内フィルタで。

落とし穴 5: 過度なネスト

3 段階以上のネストはメタデータ爆発。クエリ性能のために flatten を検討。


11. 実戦性能比較

1 億行 e-commerce

フォーマットサイズSELECT *SELECT price WHERE country='KR'
CSV (gzip)2.5 GB180 s180 s
JSON (gzip)4 GB300 s300 s
Avro2 GB60 s60 s
Parquet (snappy)1 GB25 s3 s
Parquet (zstd)700 MB22 s3 s
ORC (zstd)650 MB20 s2.5 s

カラム選択コスト

SELECT col_x FROM table_100cols:

フォーマット時間
Parquet1 カラムだけ読む → 100 MB I/O, 5 s
JSON全パース → 10 GB I/O, 120 s
CSV全パース → 5 GB I/O, 90 s

Parquet の Projection Pushdown の威力。


クイズで復習

Q1. Row vs Column 最大の性能差はどこから来るか?

A. 3 要素。Projection (必要カラムだけ読む)、圧縮 (同型カラムは Dictionary Encoding/RLE/Delta でよく圧縮)、ベクトル化 (同型連続で SIMD が効きキャッシュにも優しい)。これが組み合わさって OLAP で 100-1000 倍の差。OLTP は依然 row が有利。

Q2. Dremel の Repetition/Definition Level はなぜ必要か?

A. ネストデータを損失なくカラムに分解するため。Repetition Level は配列位置の復元、Definition Level は定義された最大深さ (NULL や optional の復元) を示す。この 2 整数で元の JSON を完全復元でき、数学的にも証明されている lossless encoding。これにより Parquet は Protocol Buffer/JSON にも columnar の恩恵をもたらす。

Q3. Predicate Pushdown と Bloom Filter がクエリを劇的に改善する仕組みは?

A. 最速の I/O は「行わない I/O」。Predicate Pushdown は row group の min/max で全スキップ可。Bloom Filter は user_id = 12345 のような点検索で min/max が効かない場合に機能。結果、10 GB ファイルから 100 MB だけ読む → 100 倍高速。

Q4. Apache Arrow が Parquet と根本的に異なる問題を解く理由は?

A. Parquet はディスクフォーマット (ディスク読取最小化)、Arrow はメモリフォーマット (システム間の serialization 最小化)。標準メモリレイアウトを全言語/システムが共有すれば zero-copy 交換が可能。PySpark toPandas() with Arrow は 10 倍以上、Arrow Flight は ODBC の 20 倍。現代のパイプラインは両方使う: ディスクは Parquet、メモリは Arrow。

Q5. Dictionary Encoding が効果的な理由と、Parquet がいつ諦めるか?

A. 実データのカラムは多くがカーディナリティ低。country ~200、status ~10。Dictionary Encoding は文字列を小さな整数に置換し bit-packing して約 10 倍削減。Parquet は辞書が parquet.dictionary.page.size (デフォルト 1 MB) を超えると plain encoding に自動フォールバック。UUID、URL 全体、ログ行など高カーディナリティ時。この場合 plain + zstd の方が効率的。


まとめ

要点

  1. Row vs Column: OLTP → row、OLAP → column。
  2. Parquet: ディスク標準。Dremel ベースのネスト対応。
  3. ORC: Hive のパートナー。性能近い。
  4. Arrow: メモリ標準。Zero-copy 交換。
  5. 圧縮: Dictionary Encoding, Run-Length Encoding, Delta, Bit-packing, ZSTD。
  6. ベクトル化: Columnar の本当の力。SIMD 親和。
  7. Lakehouse: Delta Lake, Iceberg, Hudi で ACID。
  8. チューニング: Row group サイズ、圧縮選択、パーティション設計。

チェックリスト

  • 分析ワークロードで CSV/JSON 使用中 → Parquet へ。
  • Parquet ファイルが 16 MB 未満 → compaction。
  • パーティション多すぎ/少なすぎ → 再設計。
  • 頻繁なフィルタカラムで整列 → Predicate Pushdown 最大化。
  • 圧縮デフォルトのまま → ZSTD を検討。
  • メモリ間転送遅い → Arrow 導入。
  • トランザクション必要 → Delta Lake/Iceberg。

最後の教訓

Stonebraker の 2005 年 C-Store から始まった columnar 革命は、20 年後にデータ産業全体を覆った。同じハードウェア、同じデータ、違う構造で 1000 倍速くなる。これがコンピュータサイエンスの美しさ。次に SQL を書くとき問おう: このデータはどういう構造で保存されているか? 答えが明確なら、速い/遅い理由も明確になる。


参考資料

현재 단락 (1/244)

1 億行の販売データ。各行は (id, timestamp, country, product, category, price, ...) で計 50 カラム。

작성 글자: 0원문 글자: 11,657작성 단락: 0/244