You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单表Over窗口+GroupBy场景下DuckDB比Polars慢3倍是否正常?

DuckDB vs Polars:窗口计算+分组聚合的性能差异疑问

在Python环境下测试了同时涉及窗口计算与分组聚合的场景,DuckDB的运行速度比Polars慢近3倍,但二者输出结果完全一致。想搞清楚:

  • 这一结果是否符合预期?毕竟DuckDB的设计定位更偏向跨数据框/表操作?
  • 还是仅仅因为我的SQL查询写法存在低效问题?

测试代码

import time

import duckdb
import numpy as np
import polars as pl

## 测试数据集生成
rng = np.random.default_rng(1)

nrows = 10_000_000
df = pl.DataFrame(
    dict(
        id=rng.integers(1, 100, nrows),
        id2=rng.integers(1, 1_000, nrows),
        v1=rng.normal(0, 1, nrows),
        v2=rng.normal(0, 1, nrows),
        v3=rng.normal(0, 1, nrows),
        v4=rng.normal(0, 1, nrows),
    )
)

## Polars实现(耗时:~1.1s)
start = time.perf_counter()
res = (
    df.select(
        [
            "id",
            "id2",
            pl.col("v1") - pl.col("v1").mean().over(["id", "id2"]),
            pl.col("v2") - pl.col("v2").mean().over(["id", "id2"]),
            pl.col("v3") - pl.col("v3").mean().over(["id", "id2"]),
            pl.col("v4") - pl.col("v4").mean().over(["id", "id2"]),
        ]
    )
    .groupby(["id", "id2"])
    .agg(
        [
            (pl.col("v1") * pl.col("v2")).sum().alias("ans1"),
            (pl.col("v3") * pl.col("v4")).sum().alias("ans2"),
        ]
    )
)
time.perf_counter() - start
# 1.0977217499166727

## DuckDB实现(耗时:~3.5s)
start = time.perf_counter()
res2 = (
    duckdb.sql(
        """
        SELECT id, id2,
        v1 - mean(v1) OVER (PARTITION BY id, id2) as v1,
        v2 - mean(v2) OVER (PARTITION BY id, id2) as v2,
        v3 - mean(v3) OVER (PARTITION BY id, id2) as v3,
        v4 - mean(v4) OVER (PARTITION BY id, id2) as v4,
        FROM df
        """
    )
    .aggregate(
        "id, id2, sum(v1 * v2) as ans1, sum(v3 * v4) as ans2",
        "id, id2",
    )
    .pl()
)
time.perf_counter() - start
# 3.549897135235369

问题解答

核心结论

这种性能差异主要是DuckDB写法未做优化导致的,而非完全符合产品定位的结果。通过调整SQL写法,DuckDB的性能可以大幅接近甚至超过Polars。

1. 你的DuckDB写法的低效点

当前写法会先生成1000万行的全量窗口计算中间结果,再对中间结果做分组聚合,额外增加了内存开销和数据流转成本。而Polars的查询引擎会自动优化执行计划:它能识别到窗口计算后立即分组的逻辑,直接在分组计算均值的同时完成后续乘积求和,跳过了全量中间数据的生成。

2. DuckDB的优化写法

写法1:合并表达式,避免全量中间表

直接将窗口计算逻辑嵌入聚合步骤,让DuckDB可以在一次遍历中完成计算:

start = time.perf_counter()
res2 = duckdb.sql("""
    SELECT 
        id, id2,
        SUM( (v1 - AVG(v1) OVER (PARTITION BY id, id2)) * (v2 - AVG(v2) OVER (PARTITION BY id, id2)) ) AS ans1,
        SUM( (v3 - AVG(v3) OVER (PARTITION BY id, id2)) * (v4 - AVG(v4) OVER (PARTITION BY id, id2)) ) AS ans2
    FROM df
    GROUP BY id, id2
""").pl()
time.perf_counter() - start

写法2:数学公式展开,进一步减少窗口计算

利用统计公式展开SUM((x-avg_x)*(y-avg_y)),避免重复计算窗口均值,性能更优:

start = time.perf_counter()
res2 = duckdb.sql("""
    WITH group_stats AS (
        SELECT 
            id, id2,
            AVG(v1) AS avg_v1, AVG(v2) AS avg_v2,
            AVG(v3) AS avg_v3, AVG(v4) AS avg_v4,
            SUM(v1*v2) AS sum_v1v2, SUM(v1) AS sum_v1, SUM(v2) AS sum_v2,
            SUM(v3*v4) AS sum_v3v4, SUM(v3) AS sum_v3, SUM(v4) AS sum_v4,
            COUNT(*) AS cnt
        FROM df
        GROUP BY id, id2
    )
    SELECT 
        id, id2,
        sum_v1v2 - avg_v1*sum_v2 - avg_v2*sum_v1 + avg_v1*avg_v2*cnt AS ans1,
        sum_v3v4 - avg_v3*sum_v4 - avg_v4*sum_v3 + avg_v3*avg_v4*cnt AS ans2
    FROM group_stats
""").pl()
time.perf_counter() - start

3. 关于产品定位的补充

DuckDB确实更擅长跨多表复杂查询(如多表JOIN、嵌套子查询),但在单表窗口+聚合场景下,只要写法优化到位,性能不会比Polars差。你的测试结果更多是写法未利用DuckDB的优化空间,而非定位问题。


内容的提问来源于stack exchange,提问作者lebesgue

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 11:37:10