Featured image of post DuckDB + Python 三件套:pandas、Polars、Arrow 无缝切换实战指南

DuckDB + Python 三件套:pandas、Polars、Arrow 无缝切换实战指南

DuckDB 如何与 pandas、Polars、Arrow 零拷贝协作?本文详解 5 个核心技巧,从 DataFrame 转换到大文件处理,让 Python 数据分析效率翻倍。

DuckDB + Python 三件套:pandas、Polars、Arrow 无缝切换实战指南

为什么你需要一个统一的 Python 数据处理工具链?

在 Python 数据分析的日常工作中,我们几乎离不开这三个库:pandasPolarsApache Arrow。pandas 是事实上的标准,Polars 凭借速度崛起,Arrow 则是列式内存格式的行业规范。

但当数据量上来时,问题就出现了:

# pandas 的经典困境
df = pd.read_csv('big_file.csv')      # 10GB 文件直接 OOM
df = df[df['amount'] > 0]              # 全量加载后才能过滤
result = df.groupby('category').sum()  # 聚合操作慢得令人发指

DuckDB 的出现改变了这个局面。 它不是一个需要部署的数据库服务器,而是一个可以嵌入 Python 进程的分析引擎。配合 pandas、Polars、Arrow 的零拷贝集成,你可以在不改变工作流的前提下获得数量级的性能提升。

本文基于频道最新推送内容,系统讲解 5 个最实用的 DuckDB + Python 集成技巧。


技巧一:pandas ↔ DuckDB 零拷贝转换

很多人不知道,DuckDB 可以直接读取 pandas DataFrame,不需要先写成 CSV 再读回来。整个过程数据不出内存,没有序列化开销。

将 DataFrame 注册为临时表

import duckdb
import pandas as pd

# 假设你已经有一个 pandas DataFrame
df = pd.read_parquet('sales.parquet')

# 直接注册为表,零拷贝
con = duckdb.connect()
con.register('sales', df)

# 用 SQL 处理,结果直接转回 DataFrame
result = con.execute("""
    SELECT 
        DATE_TRUNC('month', sale_date) AS month,
        SUM(amount) AS total,
        COUNT(*) AS orders
    FROM sales
    WHERE status = 'completed'
    GROUP BY 1
    ORDER BY 1
""").fetchdf()

print(result)

关键点

操作说明
con.register('name', dataframe)把 DataFrame 注册为 DuckDB 临时表
con.execute(...).fetchdf()查询结果直接变回 pandas DataFrame
数据传输通过 Arrow 格式零拷贝,无序列化开销

反过来也成立——DuckDB 的查询结果可以直接变成 pandas:

# 从 DuckDB 查询 S3 上的 Parquet 文件,一步到位
df_result = con.execute("""
    SELECT * FROM read_parquet('s3://bucket/data/*.parquet')
    WHERE date >= '2026-01-01'
""").fetchdf()

技巧二:Polars 用户必看 — Arrow 是桥梁

Polars 基于 Apache Arrow,而 DuckDB 原生支持 Arrow。这意味着两者之间可以零拷贝传递数据。

import duckdb
import polars as pl

# Polars DataFrame → DuckDB(通过 Arrow,零拷贝)
pl_df = pl.read_parquet('sales.parquet')
con = duckdb.connect()

# 方式 A:直接传 Arrow 表
arrow_table = pl_df.to_arrow()
con.register('sales_arrow', arrow_table)

result = con.execute("SELECT category, AVG(amount) FROM sales_arrow GROUP BY 1").fetchdf()

# 方式 B:DuckDB 结果直接转回 Polars
duckdb_result = con.execute("""
    SELECT category, SUM(amount) AS total_sales
    FROM sales_arrow
    GROUP BY 1
    HAVING total_sales > 10000
    ORDER BY total_sales DESC
""").fetch_arrow_table()

polars_result = pl.from_arrow(duckdb_result)
print(polars_result)

为什么这很重要?

  • Polars 擅长列式计算,适合快速的逐列变换和过滤
  • DuckDB 擅长复杂 SQL 查询,窗口函数、CTE、多表关联游刃有余
  • 两者通过 Arrow 连接,数据不用复制,性能损失为零

你可以用 Polars 做快速探索,用 DuckDB 做重活——这才是真正的最佳组合。


技巧三:大文件处理 — 永远不要加载到内存

这是 DuckDB 最杀手级的场景。你有 10GB 的 CSV 或 Parquet 文件,pandas 根本读不进去?

import duckdb

con = duckdb.connect()

# 直接查询磁盘上的大文件,只返回需要的结果
top_categories = con.execute("""
    SELECT 
        category,
        SUM(amount) AS total_sales,
        COUNT(*) AS order_count,
        AVG(amount) AS avg_order_value
    FROM read_csv_auto('/data/sales_2026.csv', header=true)
    GROUP BY category
    ORDER BY total_sales DESC
    LIMIT 10
""").fetchdf()

print(top_categories)

对比 pandas 的做法:

# ❌ Pandas:先把整个文件读进内存
import pandas as pd
df = pd.read_csv('/data/sales_2026.csv')  # 10GB 文件,内存爆炸
result = df.groupby('category').agg(
    total_sales=('amount', 'sum'),
    order_count=('amount', 'count'),
    avg_order_value=('amount', 'mean')
).nlargest(10, 'total_sales')

核心优势

维度pandasDuckDB
内存占用完整加载文件只读需要的列和数据
10GB 文件实际读取10GB+可能只需 200MB
谓词下推❌ 不支持✅ 自动优化
列式扫描❌ 全列加载✅ 只读所需列

DuckDB 使用谓词下推列式扫描技术,对 10GB 文件可能只需要读取 200MB 的数据。内存占用从 10GB+ 降到几十 MB,这不是优化,是架构层面的代差。


技巧四:多格式文件一键读取

DuckDB 能直接读取各种格式,不需要先转换。这在日常工作中非常实用——老板发来 Excel、同事丢给你一份 JSON、历史数据还在 SQLite 里。

import duckdb

con = duckdb.connect()

# CSV
con.execute("SELECT * FROM read_csv_auto('data.csv')")

# Excel
con.execute("SELECT * FROM read_xlsx('data.xlsx')")

# JSON
con.execute("SELECT * FROM read_json_auto('data.json')")

# Parquet
con.execute("SELECT * FROM read_parquet('data.parquet')")

# 多个 Parquet 文件自动合并
con.execute("SELECT * FROM read_parquet('s3://bucket/data/*.parquet')")

# SQLite 数据库直接查询
con.execute("SELECT * FROM sqlite_scan('legacy.db', 'users')")

# MySQL/PostgreSQL 远程查询
con.execute("SELECT * FROM mysql_scan('host=localhost', 'database', 'users')")
con.execute("SELECT * FROM postgres_scan('dbname=sales host=localhost', 'orders')")

实战案例:把 Excel 报表直接变成 SQL 查询对象

import duckdb

con = duckdb.connect()

# 老板发来一个 Excel 文件,里面有 5 个工作表
sheets = con.execute("""
    SELECT sheet_name 
    FROM read_xlsx_auto('月度报告.xlsx')
""").fetchall()

# 查询特定工作表
jan_data = con.execute("""
    SELECT * 
    FROM read_xlsx('月度报告.xlsx', sheet='1月')
""").fetchdf()

# 合并多个月份
feb_data = con.execute("""
    SELECT * 
    FROM read_xlsx('月度报告.xlsx', sheet='2月')
""").fetchdf()

combined = jan_data.vstack(feb_data) if hasattr(jan_data, 'vstack') else pd.concat([jan_data, feb_data])

技巧五:DuckDB 作为 pandas/Polars 的"加速引擎"

你不是必须放弃 pandas 或 Polars。DuckDB 可以作为它们的后端引擎,让慢操作变快。

用 SQL 替代慢的 groupby

import duckdb
import pandas as pd

# 传统 pandas 慢操作
# df.groupby('category').sum() 在大数据集上很慢

# 用 DuckDB 加速
con = duckdb.connect()
con.register('df', your_dataframe)

fast_result = con.execute("""
    SELECT 
        category,
        SUM(sales) AS total_sales,
        AVG(sales) AS avg_sales,
        COUNT(DISTINCT user_id) AS unique_users
    FROM df
    GROUP BY category
""").fetchdf()

Polars + DuckDB 混合使用

import duckdb
import polars as pl

con = duckdb.connect()

# 把 Polars DataFrame 转 Arrow 后注册
arrow_table = pl_df.to_arrow()
con.register('polars_df', arrow_table)

# 用 DuckDB 处理复杂 SQL,Polars 处理列变换
sql_result = con.execute("""
    WITH monthly AS (
        SELECT 
            DATE_TRUNC('month', date) AS month,
            SUM(amount) AS revenue
        FROM polars_df
        GROUP BY 1
    )
    SELECT 
        month,
        revenue,
        LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
        ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 
              / NULLIF(LAG(revenue) OVER (ORDER BY month), 0), 2) AS growth_pct
    FROM monthly
""").fetch_arrow_table()

final_df = pl.from_arrow(sql_result)

与传统工具对比表

操作pandasPolarsDuckDB + Python
读 CSVpd.read_csv()pl.scan_csv()read_csv_auto()
读 Parquetpd.read_parquet()pl.scan_parquet()read_parquet()
读 Excelpd.read_excel()❌ 不支持read_xlsx()
读 JSONpd.read_json()pl.scan_ndjson()read_json_auto()
DataFrame→表con.register()
查询→DataFrame.fetchdf()
查询→Polars.fetch_arrow_table()
大文件聚合groupby().sum() OOMgroup_by().sum()SQL GROUP BY 流式
窗口函数transform()over()OVER()
远程数据源需额外库需额外库内置支持

常用函数速查表

功能pandasDuckDB SQL
读取 CSVpd.read_csv()read_csv_auto('file.csv')
读取 Parquetpd.read_parquet()read_parquet('file.parquet')
读取 Excelpd.read_excel()read_xlsx('file.xlsx')
读取 JSONpd.read_json()read_json_auto('file.json')
注册 DataFrame不支持con.register('name', df)
结果转 DataFrameN/A.fetchdf()
结果转 PolarsN/A.fetch_arrow_table() + pl.from_arrow()
分组聚合df.groupby().agg()SELECT ... GROUP BY
窗口函数df.transform()LAG()/ROW_NUMBER() OVER()
日期截断dt.floor()DATE_TRUNC('month', date)

变现建议

掌握 DuckDB + Python 生态的整合能力,可以衍生出多种商业价值:

1. 数据服务产品化

将常用的数据处理流程封装成可复用的 Python 包或 API 服务。面向中小企业提供「数据清洗 + 报表生成」的一站式解决方案,按月收费 ¥299-999。

2. 技术咨询与培训

很多团队还在用 pandas 硬扛大文件,内存爆炸是常态。你可以为企业提供 DuckDB 迁移咨询,帮助他们重构数据处理管道。单项目收费 ¥5,000-20,000。

3. 自动化报表 SaaS

基于 DuckDB + Python 搭建轻量级 BI 平台,支持 CSV/Excel/Parquet 上传后自动生成 SQL 分析和可视化报表。面向电商、运营团队收费 ¥99-299/月。

4. 开源项目 + 付费增值

将本文中的工具类代码开源,建立个人品牌。通过 GitHub Sponsors、付费教程、优先技术支持等方式变现。

5. 数据工程模板市场

把 DuckDB + pandas/Polars 的集成模式打包成可复用的 ETL 模板,在模板市场或知识付费平台出售,每个模板定价 ¥49-199。


总结

DuckDB + Python 的正确打开方式:

  1. pandas 用户 — 用 con.register() 把 DataFrame 变成表,用 SQL 替代慢的 groupby/merge
  2. Polars 用户 — 通过 Arrow 零拷贝传递数据,Polars 做列变换,DuckDB 做聚合和窗口函数
  3. 大文件处理 — 永远不要 pd.read_csv() 整个文件,用 DuckDB 直接查询
  4. 多格式支持 — CSV、Excel、JSON、Parquet、SQLite、MySQL、PostgreSQL 都能直接查
  5. 混合使用 — 不需要二选一,DuckDB 是 pandas/Polars 的加速引擎

记住一个原则:小数据用 pandas,大数据用 DuckDB,两者通过几行代码就能无缝切换。

📖 更多 DuckDB Python 实战技巧 → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。