Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南

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,减少一次内存来回。


七、性能避坑清单

  1. 别用 iterrows() / apply 做重计算

    改成向量化或 Polars 表达式。

  2. 别用 CSV 做分析中间格式

    CSV 无类型、无压缩、无列裁剪 → 全换成 Parquet(推荐 zstd)。

  3. Polars 别反复 collect() 同一个 LazyFrame

    scan_* 不会缓存,多次 collect 会重读文件;先 read_parquet 或落中间 Parquet。

  4. DuckDB 别 SELECT * 再 Python 里删列

    在 SQL 里只选需要的列,利用列裁剪。

  5. "零拷贝"别滥用

    • duckdb.sql(...).pl():通常走 Arrow 接口,成本低;
    • polars_df.to_arrow() 再给 DuckDB:默认兼容级别下 String 可能复制,建议直接 con.register("t", polars_df) 或 duckdb.from_arrow(polars_df)。
  6. 超内存不代表换 Spark

    先试 Polars engine="streaming" 或 DuckDB 自动 spill;TB 级、多节点、高并发才上 Spark / Trino / ClickHouse。

  7. 类型要显式

    Polars 2.0 不会像旧版那样静默强制转换类型;日期用 str.to_date(),Enum/Categorical 用专用方法,能省内存也能避 bug。

  8. 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 级日志、订单、埋点数据完全够用,不需要一上来就搭集群。


相关推荐
Nebula_g1 小时前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
程序员小孔.1 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
程序猿乐锅2 小时前
【从0-1学习Python | 第六篇】 Python 函数:定义、参数与返回值
开发语言·python·学习
G018_star sky♬2 小时前
java中的字符串常见方法
java·开发语言
熊猫钓鱼>_>2 小时前
Kotlin Multiplatform for OpenHarmony 实战:kotlinx-serialization JSON 适配
开发语言·华为·kotlin·华为云·json·harmonyos·code
MasonYyp3 小时前
声音转文字的模型
python
银河技术3 小时前
从脚本到 Operator:用 Go 构建可靠的微服务自动扩缩容控制器
开发语言·微服务·golang
GIS数据转换器5 小时前
地下空间综合管廊运营管理平台
大数据·开发语言·人工智能·经验分享·perl
Wang's Blog10 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友