Polars・DuckDB・Apache Arrowで構築するインプロセスOLAP基盤の実装と最適化
この記事でわかること
- Polars・DuckDB・Apache Arrowの3ツールを組み合わせたインプロセスOLAP基盤の設計パターン
- Apache Arrowを介したゼロコピーデータ交換の仕組みと実装方法
- DuckDB 1.5とPolars 1.39の最新機能を活用したストリーミング処理パイプラインの構築
- Parquet・Icebergとの統合によるローカルレイクハウスの実現手順
- メモリ効率とクエリ性能を両立するチューニング手法
対象読者
- 想定読者: データ処理パイプラインを構築するMLエンジニア・データエンジニア
-
必要な前提知識:
- Pythonの基礎文法(pandas経験があると理解しやすい)
- SQLの基本的な操作(SELECT / JOIN / GROUP BY)
- Parquetファイルフォーマットの概要
結論・成果
Polars・DuckDB・Apache Arrowの3ツールを組み合わせることで、Apache Sparkのようなクラスタ構成なしに、単一マシン上でGB〜TB規模のOLAP処理基盤を構築できます。公式ベンチマークによると、DuckDBはTPC-H 17%のスループット向上(v1.5.0での非ブロッキングチェックポイント導入時)を達成し、Polarsのストリーミングエンジンはクラスタ構成比で54%高速・64%低コストという結果がPolars公式ブログで報告されています。Apache Arrowのゼロコピーデータ交換により、ツール間のデータ受け渡しにかかるシリアライゼーションコストは実質ゼロです。
Apache Arrowが実現するゼロコピーデータ交換を理解する
インプロセスOLAP基盤を構築するうえで、最も重要な基盤技術がApache Arrowです。Arrowは言語非依存のカラムナメモリフォーマットを定義しており、異なるツール間でメモリ上のデータをコピーせずに共有できます。2026年2月に10周年を迎えたApache Arrowは、フォーマット仕様に破壊的変更がゼロという安定性を維持しています。
カラムナフォーマットの基本構造
従来の行指向フォーマット(CSVやJSONなど)は、1行ごとに全カラムのデータを保持します。一方、カラムナフォーマットでは同じカラムのデータが連続してメモリに配置されるため、特定カラムの集計処理でCPUキャッシュのヒット率が向上します。これはMLエンジニアがよく使うNumPyの列アクセスが行アクセスより高速な理由と同じ原理です。
# カラムナフォーマットのイメージ
# 行指向: [row1_all_cols, row2_all_cols, ...] ← キャッシュミスが多い
# 列指向: [col1_all_rows], [col2_all_rows], ... ← 連続アクセスで高速
import pyarrow as pa
# Apache Arrowテーブルの作成
table = pa.table({
"user_id": pa.array([1, 2, 3, 4, 5], type=pa.int64()),
"score": pa.array([0.95, 0.87, 0.92, 0.78, 0.99], type=pa.float64()),
"category": pa.array(["A", "B", "A", "C", "B"], type=pa.string()),
})
# 各カラムはメモリ上で連続配置 → SIMD命令で高速処理
print(f"スキーマ: {table.schema}")
print(f"行数: {table.num_rows}, カラム数: {table.num_columns}")
print(f"メモリ使用量: {table.nbytes} bytes")
ゼロコピーの仕組み
「ゼロコピー」とは、データをツール間で受け渡す際に、メモリ上のバイト列をコピーせずにポインタ(参照)だけを渡す方式です。たとえばPandasからDuckDBにDataFrameを渡す場合、従来はデータ全体のシリアライズ→デシリアライズが必要でした。Apache Arrowを共通フォーマットとして採用することで、同一メモリ領域をそのまま参照できます。
import polars as pl
import duckdb
# Polars DataFrame を作成
df_polars = pl.DataFrame({
"product_id": range(1, 100_001),
"price": [i * 1.5 for i in range(1, 100_001)],
"quantity": [i % 100 for i in range(1, 100_001)],
})
# DuckDB から Polars DataFrame を直接クエリ(ゼロコピー)
# 変数名をそのままSQL内で参照できる
result = duckdb.sql("""
SELECT
CASE
WHEN price < 500 THEN 'low'
WHEN price < 5000 THEN 'mid'
ELSE 'high'
END AS price_tier,
COUNT(*) AS count,
ROUND(AVG(price), 2) AS avg_price,
SUM(quantity) AS total_qty
FROM df_polars
GROUP BY price_tier
ORDER BY avg_price
""")
# DuckDB の結果を Polars DataFrame に変換(ゼロコピー)
df_result = result.pl()
print(df_result)
なぜこの構成を選んだか:
- PolarsのDataFrame APIで前処理し、DuckDBのSQLで複雑な分析クエリを実行する「使い分け」が可能
- データ受け渡し時のメモリコピーが不要なため、100万行規模でもオーバーヘッドが数ミリ秒以内
- pandas互換のAPIではなく、Arrow互換を前提に設計された2ツールだからこそ実現できる連携
注意点:
ゼロコピー連携を利用するには
polars[pyarrow]のインストールが必要です。pip install polarsだけではArrowバックエンドが有効にならない場合があります。pip install -U duckdb 'polars[pyarrow]'で両方をインストールしてください。
Polarsのストリーミングエンジンで大規模データを処理する
Polarsはv1.37以降、ストリーミングエンジンを大幅に強化しています。従来の「全データをメモリに載せてから処理」するアプローチに対し、ストリーミングエンジンはデータをバッチ単位で処理するため、メモリに収まらないデータセットも扱えます。Polars公式ガイドによると、ストリーミングエンジンはメモリに収まるデータでもインメモリエンジンより高速な場合があると報告されています。
Lazy APIとストリーミングの基礎
PolarsのLazy APIは、クエリを即座に実行せず、実行計画として蓄積します。.collect() を呼び出した時点で、Polarsはクエリオプティマイザが述語プッシュダウン(不要なデータの読み飛ばし)や射影プッシュダウン(不要カラムの読み飛ばし)を適用した最適な実行計画を生成します。
import polars as pl
# ストリーミングエンジンをデフォルトに設定(Polars 1.38+推奨)
pl.Config.set_engine_affinity("streaming")
# Lazy APIで実行計画を構築
# → この時点ではデータは一切読み込まれない
query = (
pl.scan_parquet("s3://my-bucket/events/*.parquet")
.filter(pl.col("event_date") >= "2026-01-01") # 述語プッシュダウン
.select("user_id", "event_type", "revenue") # 射影プッシュダウン
.group_by("event_type")
.agg(
pl.col("user_id").n_unique().alias("unique_users"),
pl.col("revenue").sum().alias("total_revenue"),
pl.col("revenue").mean().alias("avg_revenue"),
)
.sort("total_revenue", descending=True)
)
# 実行計画の確認(デバッグ用)
print(query.explain())
# ストリーミング実行(メモリに収まらないデータでも処理可能)
result = query.collect()
print(result)
Iceberg/Deltaとの統合
Polars 1.39では sink_iceberg() が追加され、ストリーミングパイプラインの結果を直接Apache Icebergテーブルに書き込めるようになりました。これにより、Polarsだけでデータレイクハウスのread/writeループが完結します。
import polars as pl
# Icebergテーブルからストリーミング読み込み
lf = pl.scan_iceberg(
"s3://my-lake/warehouse/events",
catalog="rest",
catalog_uri="http://localhost:8181",
)
# 変換パイプライン
transformed = (
lf
.filter(pl.col("status") == "completed")
.with_columns(
(pl.col("amount") * pl.col("quantity")).alias("total_value"),
pl.col("created_at").dt.date().alias("event_date"),
)
.group_by("event_date", "product_category")
.agg(
pl.col("total_value").sum().alias("daily_revenue"),
pl.len().alias("order_count"),
)
)
# 結果をIcebergテーブルに直接書き込み(ストリーミング)
transformed.sink_iceberg(
"s3://my-lake/warehouse/daily_summary",
catalog="rest",
catalog_uri="http://localhost:8181",
mode="append",
)
# Delta Lakeへの書き込みも同様にサポート(1.37+)
transformed.sink_delta(
"s3://my-lake/delta/daily_summary",
mode="overwrite",
)
注意点:
sink_iceberg()はPolars 1.39で追加された機能です。1.38以前では利用できません。また、Icebergカタログ(RESTカタログなど)の事前セットアップが必要です。ローカル開発ではpyicebergとsqliteカタログの組み合わせが手軽です。
よくある間違い: Eager APIとLazy APIの混同
Polarsを初めて使う場合、pandasの習慣で .collect() を頻繁に呼んでしまうケースがあります。これはストリーミングの利点を無効にしてしまいます。
# NG: 中間結果を毎回 collect() してしまう
df1 = pl.scan_parquet("data.parquet").filter(...).collect() # ← ここで全データロード
df2 = df1.lazy().group_by(...).agg(...).collect() # ← もう一度全データ処理
# OK: 最後まで LazyFrame のまま処理
result = (
pl.scan_parquet("data.parquet")
.filter(...)
.group_by(...)
.agg(...)
.collect() # ← 最適化された計画で一度だけ実行
)
DuckDBのSQL分析エンジンで複雑なクエリを実行する
DuckDBはC++で実装されたインプロセスOLAPデータベースです。SQLiteが行指向のOLTPに最適化されているのに対し、DuckDBはカラムナストレージとベクトル化実行エンジンにより分析クエリに特化しています。DuckDB 1.5.0ではVARIANT型やGEOMETRY型の内蔵、非ブロッキングチェックポイントなど多くの改良が加わっています。
DuckDBの基本パターン
DuckDBの大きな特徴は、ファイル・DataFrame・リモートストレージを直接SQLでクエリできる点です。JDBCドライバやデータベースサーバーの設定は不要で、Pythonスクリプト内にそのまま埋め込めます。
import duckdb
# Parquetファイルを直接SQLでクエリ(ファイル全体のロード不要)
result = duckdb.sql("""
SELECT
date_trunc('month', event_date) AS month,
COUNT(*) AS event_count,
COUNT(DISTINCT user_id) AS unique_users,
ROUND(SUM(revenue), 2) AS total_revenue
FROM read_parquet('events/*.parquet')
WHERE event_date >= '2026-01-01'
GROUP BY month
ORDER BY month
""")
print(result.show())
# S3上のParquetファイルも直接クエリ可能
remote_result = duckdb.sql("""
SELECT *
FROM read_parquet('s3://my-bucket/data/*.parquet')
WHERE category = 'electronics'
LIMIT 1000
""")
ウィンドウ関数とCTEによる分析クエリ
MLエンジニアにとって、DuckDBのSQL分析機能はpandasの groupby().transform() やPolarsの over() に相当しますが、より複雑な分析をSQLの表現力で記述できます。
import duckdb
import polars as pl
# 売上データの準備(Polars DataFrame)
sales = pl.DataFrame({
"date": ["2026-01-01", "2026-01-01", "2026-01-02", "2026-01-02", "2026-01-03"] * 200,
"store_id": [1, 2, 1, 2, 1] * 200,
"product": ["A", "B", "A", "C", "B"] * 200,
"amount": [100, 200, 150, 300, 250] * 200,
})
# DuckDB でウィンドウ関数・CTEを使った分析
analysis = duckdb.sql("""
WITH daily_summary AS (
SELECT
date,
store_id,
SUM(amount) AS daily_total,
COUNT(*) AS tx_count
FROM sales
GROUP BY date, store_id
),
with_moving_avg AS (
SELECT
*,
-- 直近3日間の移動平均
ROUND(AVG(daily_total) OVER (
PARTITION BY store_id
ORDER BY date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_3d,
-- 店舗内の累積売上
SUM(daily_total) OVER (
PARTITION BY store_id
ORDER BY date
) AS cumulative_total,
-- 前日比
ROUND(daily_total * 100.0 / LAG(daily_total) OVER (
PARTITION BY store_id ORDER BY date
) - 100, 1) AS day_over_day_pct
FROM daily_summary
)
SELECT * FROM with_moving_avg
ORDER BY store_id, date
""").pl()
print(analysis)
DuckDBのメモリ管理と自動スピル
codecentric社のベンチマークによると、140GBのParquetデータを処理した際、DuckDBのメモリ使用量は約1.3GBで安定する一方、Polarsは最大17GBまで増加したと報告されています。この差はDuckDBの自動ディスクスピル機構によるものです。
import duckdb
# DuckDBのメモリ制限を設定
con = duckdb.connect()
con.execute("SET memory_limit = '4GB'") # 使用メモリの上限
con.execute("SET temp_directory = '/tmp/duckdb'") # スピル先ディレクトリ
# メモリ制限を超えるデータでもソート・集計が可能
# DuckDB が自動的に中間結果をディスクに退避させる
result = con.sql("""
SELECT
user_segment,
COUNT(*) AS user_count,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY lifetime_value) AS median_ltv,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY lifetime_value) AS p95_ltv
FROM read_parquet('large_dataset/*.parquet')
GROUP BY user_segment
ORDER BY median_ltv DESC
""")
なぜDuckDBのメモリ管理が優れているか:
- ベクトル化実行エンジンがデータをバッチ(通常2,048行)単位で処理
- ソートや集計の中間結果が
memory_limitを超えると自動的にディスクへスピル - Polarsのストリーミングエンジンも同様の仕組みを持つが、DuckDBはこの機構をより長く成熟させてきた
制約条件:
自動スピルはソート・JOIN・集計など一部のオペレータに限定されます。すべての操作が自動的にディスクスピルするわけではないため、
memory_limitの設定値は実際のワークロードに合わせて検証が必要です。
3ツールを統合した実践的なデータパイプラインを構築する
ここまでの個別機能を組み合わせて、実践的なデータ分析パイプラインを構築してみましょう。ユースケースとして「ECサイトの売上データを日次で集計・分析し、MLモデルの特徴量として出力する」パイプラインを設計します。
パイプラインアーキテクチャ
実装例: EC売上分析パイプライン
import polars as pl
import duckdb
from pathlib import Path
from datetime import datetime
# === Phase 1: Polars で前処理 ===
pl.Config.set_engine_affinity("streaming")
def preprocess_with_polars(data_path: str) -> pl.LazyFrame:
"""Polarsで前処理パイプラインを構築する"""
return (
pl.scan_parquet(data_path)
# 型変換と基本的なクレンジング
.with_columns(
pl.col("order_date").cast(pl.Date),
pl.col("amount").cast(pl.Float64),
pl.col("user_id").cast(pl.Int64),
)
# 欠損値処理
.filter(pl.col("amount").is_not_null())
.filter(pl.col("amount") > 0)
# 特徴量エンジニアリング
.with_columns(
pl.col("order_date").dt.weekday().alias("day_of_week"),
pl.col("order_date").dt.month().alias("month"),
(pl.col("amount") * pl.col("quantity")).alias("total_value"),
# ユーザーごとの購入回数(ウィンドウ関数)
pl.len().over("user_id").alias("user_purchase_count"),
)
)
# === Phase 2: DuckDB で分析クエリ ===
def analyze_with_duckdb(df: pl.DataFrame) -> pl.DataFrame:
"""DuckDBで複雑な分析クエリを実行する"""
return duckdb.sql("""
WITH user_metrics AS (
SELECT
user_id,
COUNT(DISTINCT order_date) AS active_days,
SUM(total_value) AS lifetime_value,
AVG(total_value) AS avg_order_value,
MAX(order_date) AS last_purchase_date,
MIN(order_date) AS first_purchase_date,
-- RFM分析用の指標
DATE_DIFF('day', MAX(order_date), CURRENT_DATE) AS recency_days,
COUNT(*) AS frequency,
FROM df
GROUP BY user_id
),
user_segments AS (
SELECT
*,
NTILE(5) OVER (ORDER BY recency_days ASC) AS r_score,
NTILE(5) OVER (ORDER BY frequency DESC) AS f_score,
NTILE(5) OVER (ORDER BY lifetime_value DESC) AS m_score,
FROM user_metrics
)
SELECT
user_id,
active_days,
ROUND(lifetime_value, 2) AS lifetime_value,
ROUND(avg_order_value, 2) AS avg_order_value,
recency_days,
frequency,
r_score,
f_score,
m_score,
-- RFMスコアの合計でセグメント分類
CASE
WHEN r_score + f_score + m_score >= 13 THEN 'VIP'
WHEN r_score + f_score + m_score >= 9 THEN 'active'
WHEN r_score + f_score + m_score >= 5 THEN 'at_risk'
ELSE 'churned'
END AS segment
FROM user_segments
""").pl()
# === Phase 3: 結果の永続化 ===
def save_results(df: pl.DataFrame, output_path: str):
"""分析結果をParquetに保存する"""
df.write_parquet(
output_path,
compression="zstd", # 高圧縮率
row_group_size=100_000, # DuckDB推奨のrow groupサイズ
)
# === パイプライン実行 ===
def run_pipeline(data_path: str, output_path: str):
"""前処理 → 分析 → 保存の一連の流れを実行する"""
# Phase 1: Polars で前処理(ストリーミング)
lf = preprocess_with_polars(data_path)
df_cleaned = lf.collect()
# Phase 2: DuckDB で分析(ゼロコピー受け渡し)
df_analysis = analyze_with_duckdb(df_cleaned)
# Phase 3: 結果を永続化
save_results(df_analysis, output_path)
print(f"パイプライン完了: {len(df_analysis)} ユーザーを分析")
print(f"セグメント分布:\n{df_analysis.group_by('segment').len().sort('len', descending=True)}")
if __name__ == "__main__":
run_pipeline(
data_path="data/orders/*.parquet",
output_path="output/user_features.parquet",
)
Parquet出力のチューニング
Parquetファイルの書き出し設定は、後続のクエリ性能に大きく影響します。特にDuckDBはRow Group単位で並列スキャンするため、Row Groupサイズの設定が重要です。
| パラメータ | 推奨値 | 理由 |
|---|---|---|
row_group_size |
100,000〜1,000,000 | DuckDBはRow Group単位で並列化。小さすぎると並列効率低下 |
compression |
zstd |
圧縮率と展開速度のバランスに優れる |
| パーティション | 日付・カテゴリ | 述語プッシュダウンでスキャン範囲を限定可能 |
codecentric社のベンチマークでは、Row Groupサイズが5,000行以下の場合にDuckDBの性能が5〜10倍劣化すると報告されています。デフォルトの設定を変更せずに使うのではなく、ワークロードに合わせたチューニングを推奨します。
パフォーマンスチューニングとトラブルシューティング
3ツールを組み合わせる場合、各ツールの得意・不得意を理解したうえで適切に使い分けることが重要です。ここでは実践的なチューニング手法とよくある問題への対処法を解説します。
ツール使い分けの指針
| ユースケース | 推奨ツール | 理由 |
|---|---|---|
| カラム操作・型変換・前処理 | Polars | DataFrame APIが直感的、Lazy最適化が強力 |
| 複雑なSQL分析(JOIN・ウィンドウ関数) | DuckDB | SQL表現力、クエリオプティマイザの成熟度 |
| ファイル直接クエリ(アドホック分析) | DuckDB |
read_parquet() でファイルを直接SQL |
| 大規模ストリーミングETL | Polars |
sink_parquet() / sink_iceberg()
|
| メモリ制約が厳しい環境 | DuckDB | 自動ディスクスピルが成熟 |
| MLパイプラインの特徴量変換 | Polars | Python APIとの親和性 |
Polarsのパフォーマンスチューニング
import polars as pl
import os
# 1. ストリーミングエンジンの有効化(最優先)
pl.Config.set_engine_affinity("streaming")
# 2. クラウドストレージからの読み込み時は非同期モードを有効化
os.environ["POLARS_FORCE_ASYNC"] = "1"
# メモリマップではなくObjectStore経由で読み込み
# → 大規模ファイルでのメモリ使用量が安定する
# 3. 射影プッシュダウンを活用(必要なカラムだけ読み込む)
lf = (
pl.scan_parquet("large_data/*.parquet")
.select("user_id", "amount", "date") # 3カラムだけ読み込む
.filter(pl.col("date") >= "2026-01-01")
)
# 4. 実行計画を確認して最適化を検証
print(lf.explain())
# 出力に "FILTER PUSHDOWN" や "PROJECTION PUSHDOWN" が含まれていることを確認
DuckDBのパフォーマンスチューニング
import duckdb
con = duckdb.connect()
# 1. スレッド数の調整(デフォルトは全CPUコア)
con.execute("SET threads = 8")
# 2. メモリ制限の設定
con.execute("SET memory_limit = '8GB'")
# 3. プログレスバーの有効化(長時間クエリの進捗確認)
con.execute("SET enable_progress_bar = true")
con.execute("SET enable_progress_bar_print = false")
# 4. EXPLAIN ANALYZE でボトルネックを特定
con.execute("""
EXPLAIN ANALYZE
SELECT user_id, SUM(amount)
FROM read_parquet('data/*.parquet')
GROUP BY user_id
HAVING SUM(amount) > 1000
""").show()
よくある問題と解決方法
| 問題 | 原因 | 解決方法 |
|---|---|---|
PolarsError: OutOfMemory |
Eager実行でデータ全体をロード |
scan_parquet() + Lazy APIに切り替え、set_engine_affinity("streaming") を設定 |
| DuckDBのクエリが遅い | Row Groupサイズが小さい | Parquet出力時に row_group_size=100_000 以上を指定 |
| ゼロコピーが効かない |
pyarrow 未インストール |
pip install 'polars[pyarrow]' で再インストール |
| S3からの読み込みが遅い | 同期読み込みになっている | Polars: POLARS_FORCE_ASYNC=1、DuckDB: httpfs 拡張をロード |
| JOINでメモリ不足 | 大規模テーブル同士のJOIN | DuckDB側で実行(自動スピル対応)、またはPolarsのストリーミングマージJOIN(1.38+)を使用 |
| 型不一致エラー | Polars↔DuckDB間の型マッピング | Arrow型を明示的に指定(pa.int64(), pa.float64() など) |
ハマりポイント: DuckDBのインメモリDBとファイルDBの違い
import duckdb
# インメモリDB(デフォルト) — プロセス終了でデータ消失
con_memory = duckdb.connect()
# ファイルDB — データが永続化される
con_file = duckdb.connect("analytics.duckdb")
# よくある間違い: duckdb.sql() はグローバルのインメモリDBを使う
# 明示的にファイルDBを使いたい場合は con.sql() を使用
con_file.sql("CREATE TABLE IF NOT EXISTS results AS SELECT 1 AS id")
con_file.close()
Sparkとの使い分けとトレードオフを理解する
Polars・DuckDB・Apache Arrowの組み合わせは強力ですが、すべてのユースケースに適しているわけではありません。ここではApache Sparkとの使い分けについて整理します。
| 観点 | Polars + DuckDB | Apache Spark |
|---|---|---|
| 実行モデル | シングルマシン(インプロセス) | 分散クラスタ |
| データ規模 | 〜数百GB(単一マシンのメモリ+ディスク) | 数TB〜PB |
| セットアップ |
pip install のみ |
JVM + クラスタマネージャ |
| レイテンシ | ミリ秒〜秒単位 | 秒〜分単位(JVM起動コスト) |
| コスト | 単一インスタンスの費用のみ | クラスタ維持費 |
| ユースケース | アドホック分析、ETL、特徴量生成 | PB規模バッチ、大規模ML学習 |
Sparkが必要なケース:
- 単一マシンのメモリ+ディスクに収まらないデータ量(TB超)
- 数百ノードでの分散ML学習(SparkML, Horovod連携)
- 既存のSparkエコシステム(Hive Metastore, YARN)との統合が必要
Polars+DuckDBで十分なケース:
- 数百GB以下のデータ分析
- ローカル開発・プロトタイピング
- CI/CDパイプラインでの軽量なデータ検証
- Embeddable Analytics(アプリケーションに分析機能を組み込み)
Miles Cole氏のブログ記事でも述べられているように、多くの組織が実際に処理しているデータ量はSparkを必要としないケースが大半です。インプロセスOLAPツールの成熟により、「とりあえずSpark」から「まずPolars+DuckDBで始めて、本当に分散が必要になったら移行する」というアプローチが2026年の現実的な選択肢になっています。
まとめと次のステップ
まとめ:
- Apache Arrowのカラムナメモリフォーマットにより、Polars・DuckDB間のデータ交換はゼロコピーで実現できる
- Polarsのストリーミングエンジン(1.38+)は、メモリに収まらないデータでもバッチ処理が可能で、Iceberg/Deltaへの直接書き込みをサポートする
- DuckDBの自動ディスクスピル機構により、メモリ制限下でも大規模な集計・ソート・JOINが実行でき、140GBデータで1.3GBのメモリ消費に抑えられるとベンチマークで報告されている
- 3ツールの使い分け指針は「前処理→Polars、SQL分析→DuckDB、データ交換→Arrow」
- Sparkが不要な数百GB以下の分析ワークロードでは、セットアップコストとレイテンシの面でインプロセスOLAP基盤が有利
次にやるべきこと:
-
pip install -U duckdb 'polars[pyarrow]'で最新版をインストールし、本記事のコード例を手元のデータで実行する - 既存のpandas処理をPolars Lazy APIに書き換え、
explain()でクエリ最適化の効果を確認する - Parquetファイルの
row_group_sizeを調整し、DuckDBのクエリ性能への影響をベンチマークで検証する
参考
- DuckDB公式: Polars Integration
- Polars公式: Streaming Guide
- Polars公式ブログ: Polars in Aggregate Apr 2026
- DuckDB 1.5.0リリースノート
- Apache Arrow 10周年記念記事
- codecentric: DuckDB vs Polars - Performance & Memory on Parquet Data
- Miles Cole: Should You Ditch Spark for DuckDB or Polars?
- Polars公式: Lazy API
- MarkTechPost: Building a DuckDB-Python Analytics Pipeline
注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。