Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南
摘要:很多人说"Python 做大数据慢",其实慢的是 CPython 解释器下的行级循环,不是整个生态。本文基于 2026 年主流单机数据处理栈,讲清楚 Apache Arrow、Polars、DuckDB 分别解决什么问题,并用 Parquet 实战演示"DuckDB 做 SQL 聚合 → Arrow 内存交换 → Polars 做特征工程"的混合流水线。适合在单机环境处理 GB~百 GB 级日志、订单、埋点数据,但不想一上来就搭 Spark 集群的 Python 开发者。
文章目录
- [Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南](#Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南)
-
- [一、为什么你会觉得 Python 慢](#一、为什么你会觉得 Python 慢)
- [二、Apache Arrow:多引擎之间的"通用底盘"](#二、Apache Arrow:多引擎之间的“通用底盘”)
- [三、Polars:把 DataFrame 写成"查询计划"](#三、Polars:把 DataFrame 写成“查询计划”)
-
- [3.1 Eager 模式(和 Pandas 类似)](#3.1 Eager 模式(和 Pandas 类似))
- [3.2 Lazy 模式(重点)](#3.2 Lazy 模式(重点))
- [3.3 超内存场景](#3.3 超内存场景)
- [3.4 适用场景](#3.4 适用场景)
- [四、DuckDB:把 SQL 直接查到文件上](#四、DuckDB:把 SQL 直接查到文件上)
-
- [4.1 直接查 Parquet,不先加载](#4.1 直接查 Parquet,不先加载)
- [4.2 用 SQL 做 ETL](#4.2 用 SQL 做 ETL)
- [4.3 DuckDB ↔ Polars 互操作](#4.3 DuckDB ↔ Polars 互操作)
- 五、三者怎么选(不是二选一)
- 六、生产级混合流水线实战
- 七、性能避坑清单
- 八、版本口径与参考资料
- 九、总结

一、为什么你会觉得 Python 慢
常见慢代码长这样:
python
# ❌ 慢:纯 Python 层逐行计算
import pandas as pd
df = pd.read_csv("orders.csv")
total = []
for price, qty in zip(df["price"], df["qty"]):
total.append(price * qty)
df["total"] = total
问题不在 Python 这门语言,而在:
- 数据在 CPython 里逐行走对象;
read_csv全量读入内存;- 中间 DataFrame 频繁拷贝;
- 多核 CPU 没用上(Pandas 默认受 GIL 限制)。
正确认知:
- 真正干活的是底层向量化内核(NumPy / Arrow / DuckDB C++ / Polars Rust);
- Python 只负责"描述计算";
- 写对了,Python 依然是大数据处理王牌;写错了,Spark 也救不了你。
二、Apache Arrow:多引擎之间的"通用底盘"
Apache Arrow 不是数据库,也不是计算框架,而是一套跨语言的列式内存格式。
你可以把它理解成:
不同引擎之间的"USB 接口"------Polars、DuckDB、Pandas(PyArrow 后端)、Spark、Ray 都认这个格式。
核心价值:
- 列式存储布局,CPU 缓存友好;
- 类型系统统一(Int / Float / String / Decimal / Timestamp...);
- 通过 Arrow C Data Interface / PyCapsule 在库之间传内存地址,避免一次全量序列化;
- 磁盘侧搭档是 Parquet(列式 + 压缩 + 谓词下推)。
python
# python
import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.parquet as pq
# 构建 Arrow 表(列式、强类型)
table = pa.table({
"sku": ["A100", "B200", "C300"],
"qty": [10, 20, 5],
"price": [19.9, 8.5, 120.0],
})
# 向量化计算,不写 for 循环
revenue = pc.multiply(table["qty"], table["price"])
table = table.append_column("revenue", revenue)
# 落盘 Parquet(后续给 DuckDB / Polars 直接读)
pq.write_table(table, "sales.parquet", compression="zstd")
💡 实际生产中:CSV 只用来交接原始数据,分析层统一转 Parquet。
三、Polars:把 DataFrame 写成"查询计划"
Polars 用 Rust 写核心,Python 只是前端 API。它的关键不是"快一点",而是执行模型不同。
3.1 Eager 模式(和 Pandas 类似)
python
# python
import polars as pl
df = pl.read_parquet("sales.parquet")
print(df.filter(pl.col("qty") > 8).head())
3.2 Lazy 模式(重点)
python
# python
(
pl.scan_parquet("sales/*.parquet") # 懒扫描,不立即读全量
.filter(pl.col("qty") > 8) # 过滤下推到文件扫描层
.select("sku", "qty", "price") # 列裁剪:只读用到的列
.with_columns((pl.col("qty") * pl.col("price")).alias("revenue"))
.group_by("sku")
.agg(pl.col("revenue").sum().alias("total"))
.sort("total", descending=True)
.collect() # 这一步才真正跑
)
Polars 在 collect() 前会做:
- Predicate pushdown:过滤条件下推到 Parquet row group;
- Projection pushdown:只解压用到的列;
- 多线程并行:默认吃满 CPU 核;
- Streaming:超内存时可分批执行。
3.3 超内存场景
Polars 1.x 可用:
python
# python
.collect(engine="streaming")
Polars 2.0 RC(2026-09 起)中,engine="auto" 会默认指向 streaming 引擎,内存与性能通常明显优于旧 in-memory 默认,但 join / group_by / unpivot 等操作的行顺序不再保证 ,需要保序时显式写 maintain_order=True 或 collect(engine="in-memory")。
python
# python
# 2.0 口径:需要可观测行顺序时显式声明
(
lf.join(other, on="k", how="left", maintain_order="left")
.collect()
)
3.4 适用场景
- 单机 1GB~50GB 清洗 / 特征工程;
- 多列变换、window 函数、join;
- 替代"Pandas + 各种 apply"的慢管道。
⚠️ 注意:Polars 不是 Pandas 的 drop-in 替换------没有 index、不自动按标签对齐、类型更严格;2.0 后进一步 fail-fast,隐式类型强制会直接报错。
四、DuckDB:把 SQL 直接查到文件上
DuckDB 是"进程内 OLAP 数据库",类似"给分析用的 SQLite"。
4.1 直接查 Parquet,不先加载
python
# python
import duckdb
# 直接扫目录下的 Parquet 分区文件
result = duckdb.sql("""
SELECT sku,
SUM(qty * price) AS gmv,
COUNT(*) AS orders
FROM 'sales/*.parquet'
WHERE qty > 8
GROUP BY sku
ORDER BY gmv DESC
LIMIT 10
""")
print(result.df()) # 转 Pandas
print(result.pl()) # 转 Polars(走 Arrow 交换)
优势:
- 不需要起服务、不改生产架构;
- 支持 Parquet / CSV / JSON / S3 / Iceberg / Delta;
- 有完整 SQL 优化器、JOIN 重排、落盘 spill;
- 复杂聚合和多表 JOIN 通常比手写 DataFrame 链路上手快。
4.2 用 SQL 做 ETL
sql
-- 一条 SQL 完成:读 CSV → 清洗 → 写 Parquet
COPY (
SELECT user_id,
DATE_TRUNC('day', event_time) AS day,
COUNT(*) AS pv
FROM read_csv_auto('raw_click.csv', header=true)
WHERE event_time IS NOT NULL
GROUP BY user_id, day
) TO 'click_daily.parquet' (FORMAT PARQUET);
4.3 DuckDB ↔ Polars 互操作
DuckDB 官方明确支持:直接查当前作用域里的 Polars DataFrame,以及把结果用 .pl() 拿回 Polars DataFrame,底层走 Apache Arrow 高效集成。
python
# python
import duckdb
import polars as pl
df = pl.DataFrame({"id": [1, 2, 3], "amount": [10, 20, 30]})
# 把 Polars DataFrame 当表查
out = duckdb.sql("SELECT id, amount * 1.13 AS amount_with_tax FROM df").pl()
print(out)
说明:DuckDB 与 Polars 均基于 Arrow 内存模型,结果转换成本低;但是否"完全零拷贝"取决于具体类型与兼容级别,String 列在某些
to_arrow()默认兼容级别下仍可能重建 buffer,因此更稳妥的做法是duckdb.sql(...).pl()或把 Polars Frame 直接注册/传给 DuckDB,而不是先to_arrow()再from_arrow()。
五、三者怎么选(不是二选一)
| 工具 | 角色定位 | 最适合 | 不适合 |
|---|---|---|---|
| Pandas | 生态型 DataFrame | 小数据探索、ML 前处理、教学 | 超内存、重聚合、宽表多列计算 |
| Polars | 向量化 DataFrame 引擎 | 单机大数据清洗、特征工程、流式处理 | 需要复杂 SQL 报表、事务写入 |
| DuckDB | 进程内 OLAP SQL 引擎 | Ad-hoc 分析、直接查文件、SQL 报表 | 高并发写、分布式跨节点 |
| Arrow / Parquet | 内存/磁盘交换标准 | 多引擎协作、避免拷贝、冷数据归档 | 不适合直接做业务系统主存储 |
经验公式:
- < 200MB 且要画图画模型 → Pandas 够用;
- 1GB~30GB 的 Python 管道 → Polars 主刀;
- 要 JOIN / 窗口函数 / 即席查询文件 → DuckDB 主刀;
- 多工具协作 → Arrow 做桥梁,Parquet 做落盘。
Polars 与 DuckDB 不是竞争关系,生产里常一起用:DuckDB 做即席 SQL 探索与聚合,Polars 做确定性 ETL 与复杂列表达式。
六、生产级混合流水线实战
text
原始 CSV / Kafka dump
↓ 一次性落地
Parquet(zstd 压缩、按日期分区)
↓
DuckDB:SQL 做过滤、聚合、多表 JOIN
↓ .pl() 走 Arrow 内存(避免转 CSV/Pandas 拷贝)
Polars:做复杂列表达式、特征构造、类型校验
↓
小结果 → Pandas / Plotly / Sklearn
大结果 → sink_parquet 写回数据湖
完整示例
python
# python
import duckdb
import polars as pl
# 1)DuckDB 做重 SQL 聚合
con = duckdb.connect()
daily = con.execute("""
SELECT sku,
DATE_TRUNC('day', sold_at) AS day,
SUM(qty) AS units,
SUM(qty * price) AS gmv
FROM read_parquet('data/sales/*.parquet')
GROUP BY 1, 2
""").pl() # 直接拿 Polars DataFrame,Arrow 层交换
# 2)Polars 做特征列
features = (
daily
.sort(["sku", "day"])
.with_columns([
# 近 7 天滑动 GMV
pl.col("gmv").rolling_sum(window_size=7).over("sku").alias("gmv_7d"),
# 同 sku 内当天 GMV 排名
pl.col("gmv").rank(method="dense", descending=True).over("sku").alias("rank_in_sku"),
# 日均客单价
(pl.col("gmv") / pl.col("units")).alias("avg_price"),
])
.filter(pl.col("units") >= 3)
)
# 3)写回,不回 Pandas 中转
features.sink_parquet("data/sku_features.parquet")
关键:DuckDB 结果用
.pl()而不是.df()再转 Polars,减少一次内存来回。
七、性能避坑清单
-
别用
iterrows()/apply做重计算改成向量化或 Polars 表达式。
-
别用 CSV 做分析中间格式
CSV 无类型、无压缩、无列裁剪 → 全换成 Parquet(推荐
zstd)。 -
Polars 别反复
collect()同一个 LazyFramescan_*不会缓存,多次 collect 会重读文件;先read_parquet或落中间 Parquet。 -
DuckDB 别
SELECT *再 Python 里删列在 SQL 里只选需要的列,利用列裁剪。
-
"零拷贝"别滥用
duckdb.sql(...).pl():通常走 Arrow 接口,成本低;polars_df.to_arrow()再给 DuckDB:默认兼容级别下 String 可能复制,建议直接con.register("t", polars_df)或duckdb.from_arrow(polars_df)。
-
超内存不代表换 Spark
先试 Polars
engine="streaming"或 DuckDB 自动 spill;TB 级、多节点、高并发才上 Spark / Trino / ClickHouse。 -
类型要显式
Polars 2.0 不会像旧版那样静默强制转换类型;日期用
str.to_date(),Enum/Categorical 用专用方法,能省内存也能避 bug。 -
Polars 2.0 注意行顺序
group_by/join/unpivot默认不保证行顺序,报表类输出要显式maintain_order。
八、版本口径与参考资料
本文代码按以下口径校验:
- Python 3.11+
- Polars 1.44.x 稳定线 / 2.0 RC(2026-09 起)
- DuckDB 1.5.x
- pyarrow 25.x 附近
参考与延伸:
- Polars 官方文档:Lazy / Streaming / collect(engine=...)
- Polars 2.0 发布公告与迁移说明
- DuckDB 官方 Python 文档与 Polars 集成说明
- Apache Arrow 官方定义与 Polars 内存模型说明
九、总结
Python 不慢,慢的是"用解释器干编译层该干的活"。
正确姿势是:Parquet 存、Arrow 传、DuckDB 查、Polars 算、Pandas 只做最后一段生态对接。
这套组合在单机 32G / 16 核的机器上,处理几十 GB 级日志、订单、埋点数据完全够用,不需要一上来就搭集群。