✍️ 필사 모드: Columnar ストレージ完全ガイド 2025: Parquet, ORC, Apache Arrow, Dremel — 分析 DB が 10,000 倍速い理由
日本語はじめに: 同じデータ、1000 倍の差
一つの実験
1 億行の販売データ。各行は (id, timestamp, country, product, category, price, ...) で計 50 カラム。
質問: 「韓国で売れた商品の平均価格は?」
SELECT AVG(price) FROM sales WHERE country = 'KR';
CSV や MySQL のような row-oriented ストレージ:
- 1 億行をすべて読み込む (50 カラム全部)。
countryでフィルタ。priceの平均を計算。- ディスク I/O: 数十 GB。数分かかる。
同じデータが Parquet なら:
countryカラムだけ読んでフィルタ。priceカラムだけ読んで平均計算。- ディスク 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, PostgreSQL | Parquet, 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 の例:
| Value | R | D |
|---|---|---|
| "Seoul" | 0 | 2 |
| "Busan" | 1 | 2 |
| "LA" | 0 | 2 |
| NULL | 0 | 1 |
この 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 で各バイト位置ごとにストリームを分離し、一般圧縮が効くようにする。科学データに有効。
圧縮アルゴリズム
| アルゴリズム | 圧縮率 | 速度 | 用途 |
|---|---|---|---|
| UNCOMPRESSED | 1x | 最速 | ベンチ |
| SNAPPY | 2-3x | とても速い | バランス (デフォルト) |
| LZ4 | 2-3x | とても速い | 復号が速い |
| GZIP | 4-5x | 遅い | 高圧縮 |
| BROTLI | 4-5x | 中 | Web 向け |
| ZSTD | 4-5x | 速い | 2025 推奨 |
df.write.option("compression", "zstd").parquet("data.parquet")
組み合わせ効果
1 億行の e-commerce データ:
| フォーマット | サイズ | クエリ時間 |
|---|---|---|
| CSV | 12 GB | 180 s |
| JSON | 28 GB | 300 s |
| Parquet (uncompressed) | 6 GB | 15 s |
| Parquet (snappy) | 2 GB | 8 s |
| Parquet (zstd) | 1.2 GB | 7 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
| 項目 | Parquet | ORC |
|---|---|---|
| エコシステム | Spark, Trino, Flink, Pandas | Hive, 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 GB | 180 s | 180 s |
| JSON (gzip) | 4 GB | 300 s | 300 s |
| Avro | 2 GB | 60 s | 60 s |
| Parquet (snappy) | 1 GB | 25 s | 3 s |
| Parquet (zstd) | 700 MB | 22 s | 3 s |
| ORC (zstd) | 650 MB | 20 s | 2.5 s |
カラム選択コスト
SELECT col_x FROM table_100cols:
| フォーマット | 時間 |
|---|---|
| Parquet | 1 カラムだけ読む → 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 の方が効率的。
まとめ
要点
- Row vs Column: OLTP → row、OLAP → column。
- Parquet: ディスク標準。Dremel ベースのネスト対応。
- ORC: Hive のパートナー。性能近い。
- Arrow: メモリ標準。Zero-copy 交換。
- 圧縮: Dictionary Encoding, Run-Length Encoding, Delta, Bit-packing, ZSTD。
- ベクトル化: Columnar の本当の力。SIMD 親和。
- Lakehouse: Delta Lake, Iceberg, Hudi で ACID。
- チューニング: Row group サイズ、圧縮選択、パーティション設計。
チェックリスト
- 分析ワークロードで CSV/JSON 使用中 → Parquet へ。
- Parquet ファイルが 16 MB 未満 → compaction。
- パーティション多すぎ/少なすぎ → 再設計。
- 頻繁なフィルタカラムで整列 → Predicate Pushdown 最大化。
- 圧縮デフォルトのまま → ZSTD を検討。
- メモリ間転送遅い → Arrow 導入。
- トランザクション必要 → Delta Lake/Iceberg。
最後の教訓
Stonebraker の 2005 年 C-Store から始まった columnar 革命は、20 年後にデータ産業全体を覆った。同じハードウェア、同じデータ、違う構造で 1000 倍速くなる。これがコンピュータサイエンスの美しさ。次に SQL を書くとき問おう: このデータはどういう構造で保存されているか? 答えが明確なら、速い/遅い理由も明確になる。
参考資料
- Dremel: Interactive Analysis of Web-Scale Datasets (Melnik et al., 2010)
- C-Store: A Column-oriented DBMS (Stonebraker et al., 2005)
- Apache Parquet Documentation
- Apache ORC Specification
- Apache Arrow Format
- DuckDB Paper (SIGMOD 2019)
- Delta Lake Paper (VLDB 2020)
- Apache Iceberg
- Snowflake Paper
- ClickHouse MergeTree
- Vectorization vs Compilation (Kersten et al., 2018)
현재 단락 (1/244)
1 億行の販売データ。各行は (id, timestamp, country, product, category, price, ...) で計 50 カラム。