0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Polars・DuckDB・Apache Arrowで構築するインプロセスOLAP基盤の実装と最適化

0
Last updated at Posted at 2026-04-20

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カタログなど)の事前セットアップが必要です。ローカル開発では pyicebergsqlite カタログの組み合わせが手軽です。

よくある間違い: 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のクエリ性能への影響をベンチマークで検証する

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?