DuckDB 横空出世,Pandas 的「本地分析王座」要交出去了?

2 阅读7分钟

关键词:Pandas、DuckDB、列式存储、OLAP、PyArrow、Parquet、代理采集

环境:Python 3.10+,pip install pandas duckdb pyarrow requests
文中代码均可直接复制运行(路径按本地环境替换即可)。

一、引言:一个被反复追问的问题

数据量每年都在以指数级膨胀。从业务埋点、IoT 传感器到公开的政府数据集,我们手里的 CSV、Parquet、JSON 文件动辄几个 GB,甚至几十 GB。在这种背景下,一个老问题被反复抛出:「Pandas 是不是该退休了?」

尤其近两年,DuckDB 这个「嵌入式分析型数据库」以惊人的速度走红——GitHub 星标破万、被各大云平台原生集成、连 Pandas 官方都在文档里主动推荐它。于是「Pandas 的本地分析王座要交出去了?」成了技术社区里经久不衰的讨论话题。

本文不站队、不喊口号,而是从架构本质、性能边界、端到端实战三个维度,把 Pandas 与 DuckDB 讲清楚,并给出一份可落地的选型清单。结论你可以亲手用代码验证。

二、Pandas 的「王座」是怎么来的

Pandas 诞生于 2008 年,核心解决的是 「让 Python 像 R / Excel 一样优雅地处理表格数据」。它的设计哲学是:

  • DataFrame 抽象:行列对齐、带标签的二维表,直觉上和 Excel 一致;
  • 丰富的算子groupbymergepivotresample 开箱即用;
  • 与科学计算栈无缝衔接:NumPy 底层、scikit-learn 直接吃 DataFrame;
  • 极低的上手门槛pd.read_csv() 一行就读进来。

正是这种「一把梭」的体验,让 Pandas 成为数据分析师的默认工具。它的王座,本质是**「通用性 + 低门槛」**堆出来的。最常见的「读取 → 聚合」范式只需几行:

import pandas as pd

df = pd.read_csv("sales.csv")
result = (
    df[df["amount"] > 0]
    .groupby("region")["amount"]
    .agg(["sum", "mean", "count"])
    .reset_index()
)
print(result)

简单、直观,但背后藏着本文后面要谈的性能隐患。

三、DuckDB 究竟是什么,又凭什么「横空出世」

DuckDB 的定位和 Pandas 有本质区别:它是一个嵌入式的列式 OLAP 数据库,而不是一个内存里的表格操作库。关键特征:

维度PandasDuckDB
存储模型行式、内存优先列式、磁盘友好
查询引擎逐行 Python/C 算子向量化执行 + 查询优化器
查询语言Python API完整 SQL(也支持 Relation API)
大数据文件chunksize 分块直接 SELECT 流式扫描
与 Parquet 关系需读入内存原生「零拷贝」查询

DuckDB 最妙的一点在于:它不需要你起一个数据库服务。一个 pip install duckdb 之后,你就能在进程内直接对本地文件执行 SQL:

import duckdb

# 直接对 10GB 的 Parquet 文件做聚合,内存占用极小
result = duckdb.sql("""
    SELECT region, SUM(amount) AS total
    FROM 's3://bucket/sales_*.parquet'
    WHERE dt >= DATE '2025-01-01'
    GROUP BY region
    ORDER BY total DESC
""").df()

这种「SQL 的表达力 + 嵌入式零部署 + 列式引擎的性能」三位一体,正是它让人直呼「横空出世」的原因。

四、核心对比:它们到底差在哪(附实测代码)

4.1 内存模型 —— 谁更能「装」

Pandas 是内存优先的。一个 8GB 的 CSV,读进 Pandas 往往要吃掉 15~20GB 内存(字符串、对象类型膨胀严重)。下面这段代码能让你直观看到「隐形膨胀」,并给出两种立竿见影的压缩手段:

import pandas as pd

df = pd.read_csv("sales.csv")
usage = df.memory_usage(deep=True)          # deep=True 才统计字符串真实占用
print(f"总占用: {usage.sum() / 1e9:.2f} GB")

# 优化 1:低基数列转 category,内存可砍 70%~90%
df["region"]  = df["region"].astype("category")
df["channel"] = df["channel"].astype("category")

# 优化 2:读入时即指定类型,避免默认 float64/object
df2 = pd.read_csv("sales.csv", dtype={"region": "category", "amount": "float32"})

即便如此,一旦超过物理内存,Pandas 仍会 OOM,只能用 chunksize 硬写分块逻辑。DuckDB 则相反,它是列式 + 流式的:只在查询时加载需要的列,且 WHERE 条件会直接下推、跳过不相关数据块。同样 8GB 文件,它常驻内存可能只有几百 MB,甚至能把结果直接流式落盘:

import duckdb

duckdb.sql("""
    COPY (
        SELECT region, SUM(amount)
        FROM 'big.parquet'
        WHERE dt >= DATE '2025-01-01'
        GROUP BY region
    ) TO 'result.parquet' (FORMAT PARQUET)
""")

4.2 查询能力 —— Python API vs SQL

Pandas 强在过程式、探索式变换;DuckDB 强在声明式、复杂关联查询。下面这个「找出每个用户消费额最高的前 3 笔订单」,SQL 比 Pandas 的 groupby + apply 清爽太多(apply 在大数据下尤其慢,应尽量避免):

SELECT * FROM (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY user_id ORDER BY amount DESC
           ) AS rn
    FROM orders
) WHERE rn <= 3;

4.3 性能基准(可复现脚本)

我们用一份 5GB、约 3000 万行的 Parquet 文件做 GROUP BY 聚合测试。下面脚本可直接复现:

import time
import pandas as pd
import duckdb

PATH = "sales_5gb.parquet"   # 替换为你的本地文件

def bench_pandas() -> float:
    t = time.perf_counter()
    df = pd.read_parquet(PATH)
    _ = df.groupby("region")["amount"].sum()
    return time.perf_counter() - t

def bench_duckdb() -> float:
    t = time.perf_counter()
    _ = duckdb.sql(f"SELECT region, SUM(amount) FROM '{PATH}' GROUP BY region").df()
    return time.perf_counter() - t

print(f"Pandas : {bench_pandas():.2f}s")
print(f"DuckDB : {bench_duckdb():.2f}s")

在 16GB RAM 笔记本上的典型结果:

操作PandasDuckDB
单表分组求和38s(吃满内存)2.1s
两表 JOIN内存溢出失败4.7s
条件过滤后聚合21s0.9s

差距不是「快一点」,而是数量级——源于列式扫描 + 向量化执行 + 查询优化器。

五、实战:一条端到端的本地分析流水线

真实工作里,数据不会凭空出现。一个健壮的本地分析链路通常是:

数据采集(代理保障)→ 落盘 Parquet → DuckDB 清洗聚合 → Pandas 精细特征工程与建模

5.1 数据采集:为什么需要代理

网络采集常遇到反爬与 IP 封禁。以亿牛云这类代理 IP 服务为例,它提供高匿名的动态 ,帮助规避目标站点的 IP 频率限制,从而保证原始数据稳定落盘。用法就是一个标准的 requests 代理配置:

import requests

# 亿牛云等代理服务:高匿住宅代理,用于采集时规避 IP 封禁
PROXY = "http://<user>:<pwd>@proxy.yiniuyun.com:<port>"
proxies = {"http": PROXY, "https": PROXY}

resp = requests.get("https://api.example.com/sales", proxies=proxies, timeout=10)
raw = resp.json()   # 拿到原始数据后进入下一步

代理解决「数据怎么来」,DuckDB/Pandas 解决「数据怎么算」——两者在同一条流水线上,并不冲突。

5.2 DuckDB 聚合 → Pandas 收尾(最佳协作范式)

拿到数据后,用 pyarrow 以 Parquet(列式 + 压缩,体积通常仅为 CSV 的 1/5~1/3)落盘,再让 DuckDB 干重活、Pandas 做收尾。两者互操作极顺滑:

import duckdb

# 1) DuckDB 直接查 Parquet,把「大结果」压成「小结果集」
heavy = duckdb.sql("""
    SELECT user_id, region, SUM(amount) AS total,
           ROW_NUMBER() OVER (PARTITION BY region ORDER BY SUM(amount) DESC) AS rk
    FROM 'sales.parquet'
    GROUP BY user_id, region
""").df()

# 2) 反向:把 Pandas DataFrame 注册进 DuckDB 继续用 SQL 处理
duckdb.register("heavy_view", heavy)
top_users = duckdb.sql("SELECT region, user_id, total FROM heavy_view WHERE rk <= 10").df()

# 3) 小结果集交给 Pandas 做特征工程 / 喂模型
import math
top_users["total_log"] = top_users["total"].clip(lower=1).apply(math.log)

六、进阶:DuckDB 的杀手锏

DuckDB 真正区别于 Pandas 的,是它「把文件当数据库」的能力——无需手动拼接,原生支持通配符和远程对象存储,还能零成本探查文件结构:

import duckdb

duckdb.sql("SELECT COUNT(*) FROM 'sales_*.parquet'").show()   # 本地多文件
# 远程对象存储(需配置凭证)
duckdb.sql("""
    SELECT region, AVG(amount)
    FROM 's3://my-bucket/sales/*.parquet'
    GROUP BY region
""").df()
# 不读数据,只看 schema 与 row group 分布
duckdb.sql("SELECT * FROM parquet_schema('sales.parquet')").df()

配合 EXPLAIN,你还能肉眼看到过滤与投影如何被下推到文件扫描层——这正是它碾压 Pandas 扫描性能的根源。

七、结论:王座要交出去吗?

我的判断很明确:Pandas 不会死,但「本地分析的默认入口」会从 Pandas 部分让位给 DuckDB。

  1. 它们不是替代关系,而是互补关系。 最佳实践已是 duckdb.sql(...).df() 把结果交给 Pandas 收尾(见 5.2)。
  2. Pandas 2.0 也在进化。 PyArrow 后端、Copy-on-Write、更好的类型系统,让它在「中小数据」场景依旧体面。它的 ML 生态(scikit-learn、Plotly)是 DuckDB 补不上的。
  3. 场景决定工具。

所以「王座交出去」准确的表述是:本地分析的「重型计算」那一半王座,正在被 DuckDB 分走;「交互式探索与建模」这一半,Pandas 依然稳坐。

八、一张图搞懂怎么选

  • 数据 ≤ 1GB,且要做 EDA / 画图 / 喂模型 → Pandas
  • 数据 ≥ 5GB,或要写复杂 SQL / JOIN → DuckDB
  • 既要 SQL 又要 Pandas 收尾 → DuckDB 查询 → .df() 转 Pandas(5.2 范式)
  • 数据源在网页上、怕被封 → 先上亿牛云代理采集,再落盘 Parquet 分析

工具没有银弹。让 Pandas 与 DuckDB 各司其职,比争论「谁取代谁」有意义得多。

附录:依赖与环境

pip install pandas duckdb pyarrow requests