深夜2時のメモリリーク悪夢
深夜2時、ノートPCのファンが離陸前のジェットエンジンのような轟音を立てていた。15GBのCSVファイルに対してPandasで単純な集計を実行しようとしていたときのことだ。32GBのRAMは使用率99%に張り付き、スワップ領域はスラッシングを繰り返し、そして避けられない結末が訪れた:MemoryError。カーネルが落ち、30分間の処理が一瞬で消え去った。
これまでのキャリアで、WebアプリにはMySQL、信頼性ではPostgres、非構造化データにはMongoDBと使い分けてきた。しかし、ローカルでのデータ処理となると話は別だ。本格的なRDBMSを持ち込むのは、大砲でハエを叩くようなものだ。バックグラウンドサービスの管理も、接続文字列の設定も、遅いCOPY FROMコマンドの待機も御免こうむる。ただデータをクエリしたいだけなのだ。
そこでDuckDBと出会った。「分析向けのSQLite」と思えばいい。Pythonプロセスの内部で動作し、設定ゼロで、Pandasが音を上げるようなデータセットも軽々と処理してしまう。
なぜPandasが万能ではないのか
Pandasは業界標準のツールだが、重い「メモリ税」を伴う。行指向のライブラリであり、全バイトをRAMに押し込もうとする。データセットが5GBであっても、オブジェクトのオーバーヘッドや内部コピーによって、Pandasは15〜20GBものメモリを消費することがある。
一般的な代替手段はSQLiteだが、これはトランザクション処理(OLTP)向けに最適化された行指向データベースだ。1億行の平均価格を計算するようなクエリを投げると、SQLiteはすべての行の全カラムをスキャンしなければならない。データサイエンスの用途としては非常に非効率だ。
DuckDBは分析処理(OLAP)向けに設計されたカラム指向アーキテクチャでこの状況を一変させる。「Price」カラムの平均値だけが必要なら、DuckDBはディスク上の他のデータを一切読み飛ばす。ベクトル化されたクエリ実行と組み合わさることで、SnowflakeやBigQueryに匹敵するパフォーマンスを、ローカルCPUだけで実現できる。
DuckDBの実態:メリットとデメリット
優れている点
- 依存関係ゼロ:単一のC++バイナリだ。Pythonユーザーなら
pip install duckdb一発で、外部ドライバー不要ですぐに使える。 - 速度のための設計:SIMD命令を使ったベクトル化実行により、CPUが1命令サイクルで数千行のチャンクを処理できる。
- SQLネイティブ:独自APIを覚える必要はない。
SELECT文が書けるなら、DuckDBの使い方はすでに知っている。 - ファイル直接アクセス:CSV、Parquet、JSONファイルを直接クエリできる。「インポート」の手順は不要だ。
- 高い圧縮率:1GBのCSVファイルが、DuckDBのネイティブ形式で保存すると150MB以下になることも珍しくない。
苦手な点
- 高い同時接続には不向き:SQLiteと同様、複数ユーザーが同時に同じファイルに書き込む用途には設計されていない。アナリストやエンジニア向けのツールであり、SNSサイトのバックエンドではない。
- 型に厳格:Pandasはデータ型に寛容だが、DuckDBはそうではない。最初は煩わしく感じるかもしれないが、数値カラムに紛れ込んだ文字列による計算ミスを未然に防いでくれることに、そのうち感謝することになる。
推奨セットアップ
DuckDBのセットアップはコーヒーを一杯淹れるより早く終わる。ローカルデータエンジニアリング向けのシンプルな構成としてこれをお勧めする:
# 新しい仮想環境を作成する
python -m venv venv
source venv/bin/activate
# DuckDBとモダンなデータスタックをインストールする
pip install duckdb pandas pyarrow
コマンドラインが好みなら、DuckDB CLIを入手しよう。macOSならbrew install duckdbで強力なターミナルインターフェースが手に入り、Pythonコードを一行も書かずにローファイルに対してSQLを実行できる。
実装:Pandasからの移行
実際の違いを見てみよう。logs.csvという名前の10GBのファイルがあるとする。従来のPandasによるアプローチは通常こうなる:
import pandas as pd
# 標準的なノートPCではクラッシュする可能性が高い
df = pd.read_csv('logs.csv')
result = df.groupby('status').agg({'response_time': 'mean'})
DuckDBでは、ファイルは仮想テーブルとして扱われる。データをストリーミング処理するため、10GB全体をRAMに読み込む必要が一切ない:
import duckdb
# DuckDBはファイルヘッダーをスキャンし、チャンク単位で処理する
query = """
SELECT status, AVG(response_time)
FROM 'logs.csv'
GROUP BY status
"""
result = duckdb.sql(query).df()
print(result)
「ゼロコピー」の優位性
DuckDBは既存のPandas DataFrameやArrowテーブルをコピーなしでクエリできる。データがすでに存在するメモリアドレスを直接参照するため、SQLを使って超高速でDataFrameをフィルタリングできる。
import pandas as pd
import duckdb
my_df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# DuckDBはPythonスコープ内のmy_df変数を自動的に「認識」する
result = duckdb.sql("SELECT SUM(a) FROM my_df").fetchone()
print(result[0])
大規模Parquetデータセットの処理
現代のデータパイプラインがCSVに頼ることはほとんどない。DuckDBはパーティション分割されたParquetフォルダの読み込みが得意だ。グロブパターンを使えば、数百ファイルにまたがる数十億行を数秒で集計できる:
-- CLIでもPython APIでも動作する
SELECT
count(*),
sum(total_sales)
FROM 'data/sales/*/*.parquet'
WHERE region = 'APAC';
まとめ
次にPandasが大きなファイルの解析に苦しんでいるのをじっと眺めているとしたら、そこで止まってほしい。重いDockerコンテナやクラウドクラスターに手を伸ばす前に、まずDuckDBを試してみてほしい。コーヒーを飲みながら標準的なノートPCで1億行を処理できるようになり、私のワークフローは劇的に変わった。
本番環境のPostgresインスタンスを置き換えるものではない。しかし、データ探索、CI/CDパイプライン、ローカルでの特徴量エンジニアリングにおいては、モダンなデータスタックの中で最も効率的なツールと言えるだろう。

