Pandas、Polars、DuckDB 百万行 CSV 基准测试性能对比

问AI · 混合使用三种工具的分工奥秘是什么?

Pandas、Polars 和 DuckDB 都能在 Python 数据处理中承担分析任务,但三者的执行方式和资源占用差异很大。面对同一份大型 CSV,Pandas 的内存模型容易成为瓶颈;Polars 依靠多线程与惰性求值缩短处理时间;DuckDB 则把文件查询交给进程内 SQL 引擎,并支持超出内存容量的数据。

本文使用同一份 2.3GB、约 120 万行的 CSV,在相同机器上依次测试这三种工具。测试覆盖文件读取、筛选、分组、聚合、连接和排序,并比较完整任务耗时、内存峰值与 CPU 利用情况。除了性能数据,后面也会说明各自的生态限制、适用范围以及混合 Pipeline 的分工方式。

测试结果的差距很大:Pandas 的内存峰值达到 7.2GB,Pipeline 直接崩溃了;Polars 在 8.7 秒内完成处理,内存峰值为 1.8GB;DuckDB 的完整查询耗时 12 毫秒,内存峰值只有 0.4GB。

图片

测试设置:同一工作负载下的三组对比

三种工具处理同一份 2.3GB CSV,执行完全相同的操作:读取文件,筛选 revenue 高于 1000 的行,按 region 和 quarter 分组,对 revenue 求和并统计交易数量;随后与一张约有 50k 行的查找表连接,再按总 revenue 降序排列。

测试内容是常见的分析与聚合任务:这类工作本不该要求一个分布式系统。所以测试机器只是一台每月 40 美元的 DigitalOcean Droplet,配有 4 个 vCPU 和 8GB 内存。

Pandas

我真的喜欢 Pandas。数据科学的每个教程都从 import pandas as pd 开始。不过,Pandas 的问题是所有内容都会载入内存,包括每一行、每一列,以及每个作为 Python 对象存储并带有 49 字节开销的字符串。一个 2.3GB CSV 进入 Pandas 后不会继续保持 2.3GB。字符串变成对象,整数变成 int64;整数列里只要有一个缺失值,整列就成了 float64。

测试时,单是读取文件就花了 47 秒。groupby 开始后,内存使用量升至 6.8GB;merge 阶段达到 7.2GB;接着还要排序。服务器开始使用交换空间,然后OOM killer 。

我也试过优化:指定 dtypes,将字符串设为 category,把 float64 降成 float32。内存降到了约 4.5GB,任务最终在 89 秒内完成,但整套方案非常的脆弱,数值列中只要混入一个意外的字符串流程就会崩掉。

那么换成 10 亿行会怎样?Pandas 不只是变慢,而是直接死掉:内存耗尽、崩溃、结束,即便机器配有 64GB 内存也是如此。

Pandas 在出问题之前一直很好用,只不过数据量不能太大

Polars

Polars 用 Rust 编写,采用 Apache Arrow 列式格式(columnar format),默认启用多线程,还带有一个会在查询运行前执行优化的惰性求值(lazy evaluation)引擎。Pandas 没有的这些特性,它都有(最新版的pandas也用 Arrow 列式格式了)。

同一台机器、同一个 CSV、同一套工作负载,Polars 在 3.2 秒内读完文件。读取、筛选、groupby、merge、排序这一整条 Pipeline 共耗时 8.7 秒,内存峰值为 1.8GB,比 Pandas 少 60%,而且没有崩溃。

关键在于惰性 API。代码写成这样:

 importpolarsaspl  
   
 result= (  
     pl.scan_csv("data.csv")  
     .filter(pl.col("revenue">1000)  
     .group_by(["region""quarter"])  
     .agg([  
         pl.col("revenue").sum(),  
         pl.col("transactions").count()  
     ])  
     .join(lookupon="region_id")  
     .sort("revenue_sum"descending=True)  
     .collect()  
 )

调用 .collect() 之前,Polars 不会执行任何操作。它先构建查询计划(query plan),做过滤下推(filter pushdown)并剔除未使用的列。若改用 .collect(engine="streaming"),数据还能分块流式处理。

H2O.ai 针对 1000 万行数据的基准测试显示,Polars 完成一次 groupby 需要 0.45 秒,Pandas 则要 12.5 秒,速度相差 28 倍。数据量达到 10 亿行后,前者可以在 45 秒内流式处理完毕;同一台 64GB 内存的机器上,后者会因内存不足而崩溃。

不过语法 API 要花点时间适应。原来的 df['column'].mean() 要改写成 pl.col('column').mean(),确实存在学习曲线。

Polars 就是 Pandas 本应进化成的样子:速度快、节省内存,也确实会用上所有 CPU 核心。

DuckDB

DuckDB 属于进程内分析数据库(in-process analytical database),并非 DataFrame 库。它直接运行在 Python 进程中,无需启动服务器,也没有连接字符串,import duckdb 后写 SQL 即可。

我起初有些怀疑。为一个 CSV 文件写 SQL,听起来就很诧异,不过查询跑起来后就不一样了

 importduckdb  
   
 result=duckdb.sql("""  
     SELECT   
         d.region,  
         d.quarter,  
         SUM(d.revenue) as revenue_sum,  
         COUNT(*) as transaction_count  
     FROM read_csv_auto('data.csv') d  
     JOIN lookup l ON d.region_id = l.region_id  
     WHERE d.revenue > 1000  
     GROUP BY d.region, d.quarter  
     ORDER BY revenue_sum DESC  
 """).df()

不是 12 秒,是 12 毫秒。

再跑一次,11ms;又跑一次,13ms。我检查了代码,确认没有误查一张空表。

DuckDB这么快的原因在于 DuckDB 是查询引擎,而非 DataFrame 库。SQL 会编译成经过优化的执行计划,底层采用向量化执行(vectorized execution),只读取需要的列。Parquet 文件也能直接查询,不必载入内存;当查询所需空间超过内存时,数据会自动溢写到磁盘(spill to disk)。

TPC-H 基准测试中,DuckDB 在单台机器上用 1 分 16 秒完成了完整测试。Apache Spark 在 32 台机器组成的集群上花了约 8 分钟。即使将 Spark 扩展到多台机器上的 1,024 个核心,它仍赶不上 DuckDB 的单机性能。Pandas 和 Polars DataFrame 也能由 DuckDB 直接查询:

 importpandasaspd  
 importduckdb  
   
 df=pd.read_csv("data.csv")  
 result=duckdb.sql("SELECT * FROM df WHERE revenue > 1000").df()

它像是数据处理领域的一把瑞士军刀:想用 SQL 时就写 SQL,需要 DataFrame 时就用 DataFrame,两者之间切换没有任何开销。

DuckDB 不只是快,甚至快得令人难堪——快到让人觉得,以前用其他方式做这些事很可笑。

没有什么是完美的

Pandas 依旧拥有最成熟的生态系统:Scikit-learn、Matplotlib、Seaborn,还有网上数不清的教程。数据集能放进内存,需要做探索性数据分析(exploratory data analysis)时,它仍是合适的选择。Pandas 3.0 中的写时复制(Copy-on-Write,在它最终发布之后)也会改善内存安全性。

Polars 的生态系统较小。Scikit-learn 目前还不能原生接受 Polars DataFrame,表达式 API 能力丰富,却不太符合很多人的既有习惯。某些操作在 Pandas 中很容易,例如逐行 apply;Polars 有意让它们变得更难,因为这类操作违背列式模型。

DuckDB 以 SQL 为先,若讨厌 SQL那么使用起来不会轻松。它进入 Python 数据科学领域的时间也更晚。虽然DataFrame 集成已经做得不错,但还不如原生 Polars 那么无缝。遇到用代码比用 SQL 更容易表达的复杂转换,Polars 可能更顺手。

到底该用哪一个?

数据能轻松放进内存时——加载后小于 1GB——用 Pandas。需要 scikit-learn、statsmodels 等生态工具,或者只是快速做探索性分析,也选它。如果团队成员已经熟悉 Pandas,学习新 API 又不划算,同样没必要更换。

数据很大但仍能放进内存,即 1GB 到 100GB 时,可以用 Polars。它适合需要多线程性能、惰性求值和查询优化的场景,也适合生产环境中的 ETL Pipeline;内存效率是另一个选择理由。

想直接查询文件而不将其载入内存,可以选择 DuckDB。它适合偏好 SQL 或以 SQL 为主的团队,也能处理大于内存的数据,即核外处理(out-of-core processing)。如果追求最快的查询性能,工作负载又以分析为主而非逐行转换,DuckDB 更合适。

其实多数团队采用的是混合方案:DuckDB 负责数据摄取(data ingestion)和重型 SQL 分析,Polars 承担复杂转换与特征工程(feature engineering),Pandas 留给最后一公里的 ML、可视化和报告。

总结

Pandas 没有死,但它已不再是严肃数据工作的默认选择。

Pandas 适合小数据和生态兼容性,Polars 适合快速且节省内存的 DataFrame 操作,DuckDB 则擅长快得违背物理规律的 SQL 分析。

数据量不断增长可以试试 Polars;想不经加载便直接查询文件,可以试试 DuckDB;所有工作依然都用 Pandas 也没关系,只是需留意内存使用量。

作者:Ramesh Kannan


点个 在看 你最好看!