Polars在条件自连接+分组聚合场景性能落后DuckDB的原因及优化方案
问题解答
一、Polars性能较差的潜在原因
- 中间数据量差异:你当前的Polars代码先基于
id和id2做全量左连接,生成的中间结果包含所有匹配(id,id2)对的id3组合,行数远超实际需要;而DuckDB将id3的过滤条件直接嵌入JOIN的ON子句,连接过程中就过滤掉无效行,中间数据集大小大幅降低,后续聚合的计算量也随之减少。 - 查询优化能力差异:DuckDB作为OLAP数据库,其查询优化器对条件连接+聚合的场景有深度优化,能自动完成谓词下推、执行计划调优;而Polars的优化器未自动将过滤条件下推到连接阶段,导致无效数据的生成和处理。
- 执行引擎适配性:DuckDB的引擎在复杂连接、聚合场景下的多核调度和内存管理更适配OLAP workload,并行执行效率更高。
二、Polars中更高效的实现方式
核心优化思路是将过滤条件下推到连接阶段,避免生成冗余中间数据,同时调整执行参数减少额外开销。优化后的代码如下:
import time import numpy as np import polars as pl rng = np.random.default_rng(1) nrows = 5_000_000 df = pl.DataFrame( dict( id=rng.integers(1, 1_000, nrows), id2=rng.integers(1, 10, nrows), id3=rng.integers(1, 500, nrows), value=rng.normal(0, 1, nrows), ) ) start = time.perf_counter() res_optimized = ( df.lazy() .join( df.lazy(), on=["id", "id2"], how="left", # 将id3的过滤条件作为连接条件的一部分,提前过滤无效行 condition=(pl.col("id3") > pl.col("id3_right")) & (pl.col("id3") - pl.col("id3_right") < 30) ) .drop_nulls() # 过滤左连接后产生的无效null行 .group_by(["id2", "id3", "id3_right"]) .agg(pl.corr("value", "value_right")) .collect() # 内存足够时关闭streaming,减少批次处理开销 ) print(time.perf_counter() - start)
关键优化点说明
- 连接阶段过滤:通过
condition参数在连接时就应用id3的过滤逻辑,从源头上减少中间数据量,这是性能提升的核心。 - 关闭不必要的流式处理:
streaming=True适合内存无法容纳全量数据的场景,但会增加调度和IO开销;在32核机器上处理500万行数据,内存通常足够,关闭流式处理能显著提升速度。 - 过滤无效null行:左连接后不符合条件的行会生成null值,
drop_nulls()能快速剔除这些行,减少后续聚合的计算量。
内容的提问来源于stack exchange,提问作者lebesgue
相关产品推荐
相关产品推荐

