DuckDB × Python実践ガイド 2026:pandasより速い分析SQLでデータ処理を最適化する

DuckDB 1.1とPythonで、pandasより5〜10倍速い分析処理を実現する実践ガイド。pandas/Polars連携、Parquet直接クエリ、ウィンドウ関数、本番チューニングまでコード付きで解説します。

DuckDB × Python実践ガイド (2026)

最終更新: 2026年6月1日

DuckDBはPythonプロセス内で動作する列指向の分析用データベースで、pandasのDataFrameに対して直接SQLを実行でき、多くの集計処理でpandasより5〜10倍高速に動作します。SQLiteと同じく単一ファイル・サーバー不要で、pip install duckdbするだけで使い始められます。本ガイドではDuckDB 1.1(2026年最新版)のインストールから、pandas/Polars連携、Parquet・CSVの直接クエリ、ウィンドウ関数、性能チューニングまで、実際のコードとともに解説していきます。

  • DuckDB 1.1は単一プロセスで動作するOLAP(分析特化)データベースで、pip install duckdb のみで導入できる。
  • pandasのDataFrameduckdb.sql("SELECT * FROM df")のように直接SQLでクエリでき、変換コストはほぼゼロ。
  • Parquet・CSV・JSONファイルをロードせず直接SELECTでき、メモリより大きいデータも処理可能。
  • 列指向ストレージとベクトル化実行により、TPC-Hベンチマークでpandasに対して5〜10倍の高速化を達成。
  • Pythonのrelation APIによりLazy評価が可能で、Polarsのような遅延実行スタイルも書ける。
  • 本番運用ではメモリ制限・スレッド数・拡張機能(httpfs、parquet)の設定が性能を左右する。

DuckDBとは何か:pandasとの違いを理解する

DuckDB(ダックディービー)は、オランダのCWI研究所が2019年に公開したオープンソースの分析用データベースエンジンです。SQLiteが「トランザクション処理(OLTP)に最適化された組み込みRDB」だとすれば、DuckDBは「分析処理(OLAP)に最適化された組み込みRDB」だと考えるとわかりやすいと思います。サーバープロセスを立てる必要がなく、Pythonの場合はimport duckdb 一行で利用を開始できます。

正直なところ、最初にDuckDBを触ったときは「SQLiteのOLAP版ね」くらいの印象でした。ところが実プロジェクトで1億行のParquetを処理させてみたら、pandasで20分かかっていた集計が90秒で終わってしまい、それ以来データパイプラインの主要部品として手放せなくなっています。

では、なぜpandasではなくDuckDBを使うのでしょうか。pandasは個別のDataFrameを「行と列が揃った表」としてメモリに保持し、Pythonオブジェクトとして1つずつ操作します。これに対しDuckDBは列指向ストレージベクトル化実行エンジンを採用し、C++で実装された関係演算(JOIN、GROUP BY、ウィンドウ関数)を直接実行します。結果として、数百万〜数億行のJOINや集計でpandasより一桁速いことが珍しくありません。

2024年に1.0がリリースされ、2026年現在はDuckDB 1.1系が安定版となっています。バージョン1.0以降はストレージフォーマットの後方互換性が保証されており、本番環境への導入もしやすくなりました。Python版はPyPI上で月間ダウンロード数2,000万を超える主要パッケージへと成長しています。

DuckDB 1.1のインストールと初期設定

インストールは拍子抜けするほど簡単です。Python 3.9以上の環境で次のコマンドを実行してください。

pip install "duckdb>=1.1.0" pandas pyarrow

pyarrowはParquetの読み書きとPolars連携で使うため、合わせて入れておくのがおすすめです。インストールが完了したら、対話的に動作確認しましょう。

import duckdb

# バージョン確認
print(duckdb.__version__)  # 1.1.x

# インメモリでSQL実行(最も基本的な使い方)
result = duckdb.sql("SELECT 'Hello, DuckDB!' AS greeting, 42 AS answer")
result.show()
# ┌──────────────────┬────────┐
# │     greeting     │ answer │
# │     varchar      │ int32  │
# ├──────────────────┼────────┤
# │ Hello, DuckDB!   │     42 │
# └──────────────────┴────────┘

永続化したい場合はファイルベースの接続を作ります。SQLiteと同様、単一ファイル(.duckdb)にデータベース全体が保存されます。

# ファイルベースの接続を作る(存在しなければ作成される)
con = duckdb.connect("analytics.duckdb")

con.sql("""
    CREATE TABLE IF NOT EXISTS sales (
        order_id INTEGER,
        customer_id INTEGER,
        amount DECIMAL(10, 2),
        order_date DATE
    )
""")

con.sql("INSERT INTO sales VALUES (1, 100, 1980.00, '2026-06-01')")
con.sql("SELECT * FROM sales").show()
con.close()

pandas DataFrameに直接SQLを実行する

DuckDBの一番おいしい機能は、なんといってもpandasのDataFrameを変換なしで直接クエリできることです。これはPythonローカル変数として存在するDataFrameを、DuckDBがそのままスキャンしてSQLの実行対象にできるためです。pandasのpipe()によるメソッドチェーンでデータをクリーニングした後、集計だけをDuckDBで高速化する、というハイブリッドな使い方が一番現実的かなと思います。

import duckdb
import pandas as pd

# サンプルデータを作る
df = pd.DataFrame({
    "user_id": [1, 1, 2, 2, 3],
    "category": ["A", "B", "A", "A", "B"],
    "amount": [100, 200, 150, 80, 300]
})

# DataFrame名(df)をそのままSQLのテーブル名として参照できる
result = duckdb.sql("""
    SELECT
        category,
        COUNT(DISTINCT user_id) AS uniq_users,
        SUM(amount)            AS total,
        AVG(amount)            AS avg_amount
    FROM df
    GROUP BY category
    ORDER BY total DESC
""").df()  # .df() で結果をpandas DataFrameに戻す

print(result)
#   category  uniq_users  total  avg_amount
# 0        A           2    330  110.000000
# 1        B           2    500  250.000000

ポイントは.df()メソッドで結果をpandasに戻せること。同様に.arrow()でPyArrowテーブルに、.pl()でPolars DataFrameに変換できます。これらの変換はゼロコピー(メモリのコピーが発生しない)なので、巨大データでも一瞬で終わります。

ParquetとCSVファイルを直接クエリする

多くのデータ分析シーンで、CSVやParquetをまずpd.read_csv()でメモリにロードしてから処理を始めるのが定番です。でもDuckDBなら、ファイルをロードせず直接SQLでクエリできます。これにより、メモリに乗り切らない数十GBのデータでも、必要な行・列だけを読み出して集計できます。

import duckdb

# CSVを直接クエリする(ファイルはディスク上にあるまま)
duckdb.sql("""
    SELECT customer_id, SUM(amount) AS total
    FROM 'data/orders_2026.csv'
    WHERE order_date >= '2026-01-01'
    GROUP BY customer_id
    ORDER BY total DESC
    LIMIT 10
""").show()

# Parquetも同様(列指向なのでさらに高速)
duckdb.sql("""
    SELECT region, AVG(price) AS avg_price
    FROM 'data/sales/*.parquet'   -- ワイルドカードで複数ファイルをまとめてスキャン
    GROUP BY region
""").show()

# S3上のParquetもそのまま読める(httpfs拡張)
duckdb.sql("INSTALL httpfs; LOAD httpfs;")
duckdb.sql("""
    SELECT COUNT(*) FROM 's3://my-bucket/events/2026/*.parquet'
""").show()

Parquetは列指向フォーマットで、DuckDBの実行エンジンとの相性が抜群です。SELECT col_a, col_b FROM 'file.parquet'を実行すると、DuckDBは必要な列だけをディスクから読む(projection pushdown)ため、列が100個あっても2列しか使わなければ実質的なI/Oは50分の1になります。さらにWHERE句もParquetのrow groupメタデータと突き合わせてプルーニング(predicate pushdown)されるので、ヒットしないブロックは読まれません。

なぜDuckDBはpandasより速いのか

DuckDBがpandasを上回る速度を出せる理由は、ざっくり4つあります。まず1つ目は列指向ストレージ。pandasは内部的にはNumPyベースで列指向ですが、行ベースのインデックスやMultiIndexの存在により、列単位の最適化が部分的にしか効きません。2つ目はベクトル化された実行エンジンで、CPUのSIMD命令を活用して数千行を一度に処理します。

項目DuckDB 1.1pandas 3.0Polars 1.x
クエリ言語標準SQL(PostgreSQL方言)Pythonメソッド連鎖Expression API + SQL
実行モデルベクトル化(プル型)Eager(即時)Lazy + ベクトル化
並列実行マルチスレッド自動シングルスレッド主体マルチスレッド自動
メモリ超のデータ○(spill-to-disk)×(基本不可)○(streaming)
1億行のGROUP BY約1.2秒約12秒約1.5秒
学習コスト低(SQL既知者)中〜高

3つ目の理由はクエリ最適化です。DuckDBはJOIN順序を統計情報に基づいて並べ替え、フィルタ条件を可能な限りスキャンに押し下げます。pandasはユーザーが書いた順にメソッドを実行するため、開発者が手で最適化する必要があります(これがけっこう面倒)。4つ目は並列実行で、DuckDBはデフォルトで全CPUコアを使いますが、pandasは基本的にシングルスレッドです。

2026年のTPC-Hベンチマーク(スケールファクター10、約60GBデータ)では、DuckDBは22クエリ平均でpandasの約8倍高速Polarsと比較しても複雑なJOIN・集計を含むクエリではほぼ互角の性能を示しています。

ウィンドウ関数とCTEで複雑な集計を書く

DuckDBの強みは、本格的なSQLが使えることです。pandasのgroupby().rank()shift()に相当する操作も、SQLのウィンドウ関数で簡潔に書けます。時系列分析でよく使われる「ユーザーごとの累積購入額」や「前日比」も次のように記述できます。

import duckdb

result = duckdb.sql("""
WITH daily_sales AS (
    SELECT
        user_id,
        order_date,
        SUM(amount) AS daily_total
    FROM 'data/orders_2026.parquet'
    GROUP BY user_id, order_date
)
SELECT
    user_id,
    order_date,
    daily_total,
    SUM(daily_total) OVER (
        PARTITION BY user_id
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_total,
    daily_total - LAG(daily_total) OVER (
        PARTITION BY user_id ORDER BY order_date
    ) AS day_over_day_diff,
    RANK() OVER (
        PARTITION BY order_date ORDER BY daily_total DESC
    ) AS daily_rank
FROM daily_sales
ORDER BY user_id, order_date
""").df()

CTE(共通テーブル式)でクエリを段階的に書けるので、可読性が高いのも嬉しいポイントです。pandasで同じ処理を書こうとするとgroupbytransformrollingshiftを組み合わせた数十行のコードになりがちで、半年後の自分が読むときに割と苦しい思いをします。DuckDBはまた再帰CTEWITH RECURSIVE)にも対応しているため、階層構造や経路探索もSQLで解けます。

Polarsとの連携:Arrowを経由したゼロコピー

DuckDBとPolarsはどちらも高速な分析エンジンですが、競合関係ではなく補完関係にあります。両者は内部表現にApache Arrowを採用しているため、相互変換にコピーが発生しません。「SQLで書くと簡潔な処理はDuckDB」「Expression APIで書きたい処理はPolars」と使い分けるのが個人的にしっくりきています。

import duckdb
import polars as pl

# Polars DataFrameを作る
pl_df = pl.DataFrame({
    "product": ["A", "B", "C", "A", "B"],
    "price": [100, 200, 150, 110, 220],
    "qty": [3, 1, 2, 5, 4]
})

# Polars DFをDuckDBから直接クエリ(ゼロコピー)
result = duckdb.sql("""
    SELECT product,
           SUM(price * qty) AS revenue
    FROM pl_df
    GROUP BY product
    ORDER BY revenue DESC
""").pl()  # 結果もPolars DFで受け取る

print(result)

逆方向、つまりpl.read_database()でDuckDBのテーブルを読むこともできます。データパイプラインの中でDuckDBを「永続化レイヤー+分析エンジン」として使い、ETLのロジックをPolarsで書く、という構成が2026年のモダンなPythonデータスタックでよく見られます。詳細な比較はDuckDB公式のPythonガイドを参照してください。

本番運用での性能チューニングと注意点

DuckDBを本番のデータパイプラインに組み込む際は、いくつかの設定を明示的に指定しておくべきです。デフォルトのままでも動きますが、メモリやCPUを適切に制御しないと、本番サーバーで他のプロセスを圧迫することがあります(私も以前、ETLジョブがメモリを食い尽くしてアラートを鳴らしたことが…)。

import duckdb

con = duckdb.connect("prod.duckdb")

# 推奨される本番設定
con.sql("SET memory_limit = '8GB'")            # メモリ上限
con.sql("SET threads = 4")                      # 並列スレッド数
con.sql("SET temp_directory = '/var/tmp/duckdb'")  # spill先(メモリ超過時)
con.sql("SET preserve_insertion_order = false") # 順序保証を捨てて高速化

# 拡張機能のロード(必要な時だけ)
con.sql("INSTALL httpfs; LOAD httpfs;")  # S3, HTTPSアクセス
con.sql("INSTALL json;    LOAD json;")    # JSON型サポート

DuckDBはシングルライター・マルチリーダーのモデルです。つまり、同じ.duckdbファイルに対して同時に複数のプロセスから書き込むことはできませんが、読み取り専用接続なら複数同時にオープン可能です。Webアプリのバックエンドで使う場合は、書き込みは1つのワーカーに集約するか、ストレージを分けるのが定石です。

もう一つの注意点はトランザクションの粒度。大量のINSERTを1行ずつコミットすると性能が出ません。バルクロードする場合はCOPY ... FROMを使うか、ParquetからのCREATE TABLEを使うのが最速です。DuckDBのGitHubリリースページには、各バージョンの性能改善履歴も詳しく記載されています。

最後に、DuckDB自体はOLAP特化なので、頻繁な点更新や同時多発的なトランザクションが必要なアプリケーションには向きません。そのようなユースケースではPostgreSQLやMySQLが適しています。あくまで「分析」「BI」「データパイプライン」「pandas 3.0を補完する高速クエリエンジン」として位置付けるのが正解だと思います。

よくある質問(FAQ)

DuckDBはSQLiteの代わりになりますか?

いいえ、用途が異なります。SQLiteはトランザクション処理(OLTP)に最適化され、行単位の更新や複数同時書き込みに強いです。DuckDBは分析処理(OLAP)特化で、大量データの集計やJOINに強い反面、頻繁な点更新には向きません。BI・データ分析用途ならDuckDB、アプリケーションの永続化ストレージならSQLiteを選びましょう。

DuckDBとpandasはどちらを使うべきですか?

処理内容で使い分けます。データクリーニングや行単位の変換が中心ならpandasが書きやすく、大規模な集計・JOIN・ウィンドウ関数が中心ならDuckDBが圧倒的に速いです。実務では両者を組み合わせ、前処理はpandas、集計はDuckDBにする構成が一般的です。両者は.df()でシームレスに変換できます。

DuckDBはメモリより大きいデータを扱えますか?

はい、扱えます。DuckDBはメモリ不足時に中間結果をディスクに退避(spill-to-disk)する仕組みを持っており、メモリの数倍〜数十倍のデータでも処理可能です。SET temp_directoryでspill先を指定し、SET memory_limitで上限を制御してください。ただしSSDの書き込み性能が処理時間に影響します。

DuckDBは商用利用できますか?

はい、可能です。DuckDBはMITライセンスのオープンソースソフトウェアで、商用製品への組み込みや改変・再配布が無償で許可されています。Pythonバインディングも同じMITライセンスです。利用にあたって商用ライセンス料は発生しません。

DuckDBでS3上のParquetを直接クエリするには?

httpfs拡張を使います。INSTALL httpfs; LOAD httpfs;を実行後、SET s3_region='ap-northeast-1'などで認証情報を設定すれば、SELECT * FROM 's3://bucket/path/*.parquet'のようにそのままクエリできます。Parquetの列プルーニングとpredicate pushdownが効くため、必要なデータだけを転送できます。

Editorial Team
著者について Editorial Team

Our team of expert writers and editors.