CSVの先へ:Parquet、Arrow、DuckDBによるハイパフォーマンスなデータエンジニアリング

Database tutorial - IT technology blog
Database tutorial - IT technology blog

I/Oの代償:なぜ従来のストレージは限界を迎えるのか

ノートPCで10GBのCSVファイルを処理しようとした初めての時のことを今でも覚えています。冷却ファンが鳴り響き、RAM使用率は95%に達し、最終的にPythonスクリプトは予想通りMemoryErrorで終了しました。

それは大きな転換点でした。MySQLやPostgreSQLのようなリレーショナルデータベースを扱ってきた人にとって、この不満は避けて通れない道です。これらのシステムは信頼できる主力選手ですが、単一の平均値を計算するために1億行をスキャンする必要がある場合、動作が鈍く感じられることがよくあります。

通常、ボトルネックはデータベースエンジン自体ではなく、ディスク上のデータの物理的なレイアウトにあります。従来のデータベースやCSVファイルは「行指向(row-oriented)」であり、1つのレコードに関するすべてのデータがまとめて保存されます。クエリが「価格(Price)」列のみを必要としている場合でも、コンピュータはすべての行に関連付けられた名前、住所、長い説明などを読み取る必要があります。これにより、パフォーマンスを低下させる膨大で不要なI/Oオーバーヘッドが発生します。

列指向の革命

列指向ストレージは、データを行ではなく列ごとにグループ化することでこれを解決します。分析ワークロードにおいて、このアーキテクチャ上の転換はすべてを変えます。

Apache Parquet:ディスク効率の王者

Apache Parquetは、高効率なストレージ金庫のようなものだと考えてください。これはオープンソースの列指向フォーマットであり、CSVをよりスマートにしたような仕組みです。

64ビット整数のリストのように、単一の列内のデータは通常似ため、Parquetはどの行ベースのフォーマットよりもはるかに効果的にデータを圧縮できます。また、「述語プッシュダウン(predicate pushdown)」もサポートしています。これにより、フィルタに一致しないデータのチャンク全体を、ディスクから読み取ることなくスキップできます。その結果、移動するデータ量が減り、結果が速く得られます。

Apache Arrow:ゼロコピーによるメモリ速度

Parquetがディスクを処理するなら、Apache Arrowはメモリを管理します。従来、データベースとPythonスクリプトのようなツールの間でデータを移動させるには「シリアライズ」が必要でした。これにはデータを両方のツールが理解できる形式にパッキングおよびアンパッキングする作業が含まれ、貴重なCPUサイクルを浪費します。

Arrowは標準化された列指向のメモリ形式を提供します。これにより、異なるシステム間でデータをコピーや変換することなく、即座に共有できるようになります。これを「ゼロコピー」読み取りと呼び、メモリのボトルネックを効果的に解消します。

DuckDB:モダンな分析エンジン

ParquetとArrowを連携させるには、エンジンが必要です。DuckDBは「分析用のSQLite」としばしば呼ばれます。これはプロセス内SQL OLAP(オンライン分析処理)データベースであり、サーバーの設定は不要です。ParquetファイルをArrowベースの実行環境でクエリするように設計されています。ローカルでのデータパイプライン構築において、今や私の第一の推奨ツールです。

ハンズオン:高速な分析パイプラインの構築

パフォーマンスの差を実際に見てみましょう。データセットを生成し、2つの形式で保存して、Pythonを使用して結果を比較します。

1. 環境構築

グローバルなPython環境を汚さないよう、仮想環境を使用してコアスタックをインストールします。

pip install pandas pyarrow duckdb numpy

2. 500万行の生成

本番環境をシミュレートするために、500万行の合成データセットを作成します。これにはID、タイムスタンプ、カテゴリ、価格が含まれます。

import pandas as pd
import numpy as np
import time

# サンプルデータの生成
num_rows = 5_000_000
data = {
    'timestamp': pd.date_range('2023-01-01', periods=num_rows, freq='S'),
    'user_id': np.random.randint(1000, 9999, size=num_rows),
    'category': np.random.choice(['電子機器', '書籍', 'ガーデニング', '玩具'], size=num_rows),
    'price': np.random.uniform(10.0, 500.0, size=num_rows),
    'quantity': np.random.randint(1, 10, size=num_rows)
}

df = pd.DataFrame(data)

# CSVとして保存
start = time.time()
df.to_csv('data.csv', index=False)
print(f"CSV書き込み時間: {time.time() - start:.2f}s")

# Parquetとして保存
start = time.time()
df.to_parquet('data.parquet', engine='pyarrow', compression='snappy')
print(f"Parquet書き込み時間: {time.time() - start:.2f}s")

3. ストレージ効率

スクリプトを実行した後、ディレクトリを確認してください。劇的な違いに気づくはずです。私のテストでは、CSVファイルは約280MBでした。Parquetファイルは?わずか60MBです。これはディスク専有面積の75%削減であり、よりスマートなデータエンコーディングのみによって達成されています。

4. クエリ速度:CSV vs. Parquet

次に、カテゴリごとの総収益を計算します。生のCSV読み取りと、DuckDBによる最適化されたParquetスキャンを比較します。

import duckdb

# CSVへのクエリ
start = time.time()
csv_result = duckdb.query("""
    SELECT category, SUM(price * quantity) as revenue 
    FROM 'data.csv' 
    GROUP BY category
""").to_df()
print(f"CSVクエリ時間: {time.time() - start:.4f}s")

# Parquetへのクエリ
start = time.time()
parquet_result = duckdb.query("""
    SELECT category, SUM(price * quantity) as revenue 
    FROM 'data.parquet' 
    GROUP BY category
""").to_df()
print(f"Parquetクエリ時間: {time.time() - start:.4f}s")

標準的なハードウェアでは、Parquetへのクエリは通常10倍から50倍高速です。DuckDBはファイル全体を読み込むような無駄なことはしません。categorypricequantity列のみをメモリに取り込み、残りは無視します。

5. Apache Arrowによるゼロコピー

データがすでにArrowテーブルとしてメモリ上にある場合、DuckDBは変換コストなし(ゼロコピー)でクエリを実行できます。これは複雑なパイプラインにおいて極めて重要です。

import pyarrow as pa

# PandasをArrowテーブルに変換
table = pa.Table.from_pandas(df)

# Arrowテーブルを直接クエリ
start = time.time()
arrow_result = duckdb.query("SELECT AVG(price) FROM table").to_df()
print(f"Arrowメモリクエリ時間: {time.time() - start:.4f}s")

結論

CSVのような行ベースの形式から列指向ストレージに移行することは、データエンジニアリングのレベルを上げる最も簡単な方法の1つです。ストレージコストを大幅に削減し、数分間の待ち時間を数秒の実行に変えます。標準的なノートPCが、ハイエンドなデータウェアハウスのように感じられるようになります。

数メガバイトを超えるデータセットにCSVを使うのはやめましょう。中間データはParquetとして保存してください。SQLを実行する必要があるときはDuckDBを使いましょう。一度このパフォーマンスの向上を体験すれば、以前のデータ処理方法に戻りたいとは思わなくなるはずです。

Share: