首页
/ Polars SQL 聚合函数权威指南:AVG、SUM、COUNT 到 QUANTILE、STRING_AGG 的 16 个内置聚合全解析

Polars SQL 聚合函数权威指南:AVG、SUM、COUNT 到 QUANTILE、STRING_AGG 的 16 个内置聚合全解析

2026-09-08 21:14:26作者:滑思眉Philip

Polars 除了提供高性能的惰性 API,还内置了一个完整的 SQL 引擎,用户可以对 DataFrame 直接执行 df.sql(...) 查询。本文以官方聚合函数参考文档 py-polars/docs/source/reference/sql/functions/aggregate.rst 为骨架,系统讲解 Polars SQL 支持的 16 个内置聚合函数:它们各自的语义、别名、与分组 GROUP BY 的配合、FILTER (WHERE ...) 条件聚合子句,以及底层 Rust 实现(crates/polars-sql/src/functions.rs)如何把这些 SQL 函数翻译为 polars-lazy 表达式。读完本文,你将能直接用 SQL 完成描述性统计、分布分析、字符串拼接与条件聚合等常见数据分析任务。

一、前置知识:如何执行一条 SQL 聚合查询

Polars 的 DataFrame 自带 sql 方法,只要把 DataFrame 当作名为 self 的表即可查询。这也是官方参考文档中所有示例使用的形式:

import polars as pl

df = pl.DataFrame({"bar": [20, 10, 30, 40]})
df.sql("SELECT AVG(bar) AS bar_avg FROM self")

运行上述代码需 Python 环境安装了 polars(SQL 功能内置于主包,无需额外扩展)。此外,Polars 还提供全局的 pl.SQLContext(见 py-polars/docs/source/reference/sql/index.rst),可在多张 DataFrame 之间执行 JOIN、子查询等复杂语句,本文示例均使用更轻量的 df.sql(...)

聚合函数通常配合 GROUP BYHAVINGORDER BY 使用(这些子句的完整说明见 clauses.rst)。下面先给出全部聚合函数的一览,再逐一深入。

二、16 个聚合函数速查表

函数 作用
AVG 返回分组内所有元素的平均值(mean)
CORR 返回两列之间的 Pearson 相关系数
COUNT 返回分组内的元素个数
COVAR 返回两列之间的协方差(样本协方差)
FIRST 返回分组内的第一个元素
LAST 返回分组内的最后一个元素
MAX 返回分组内所有元素的最大值
MEDIAN 返回分组内的中位数
MIN 返回分组内所有元素的最小值
QUANTILE_CONT 返回连续型分位数(在相邻两个值之间插值得到)
QUANTILE_DISC 返回离散型分位数(返回实际落在对应子区间的某个真实取值)
STDDEV 返回分组内所有元素的标准差(样本标准差)
STRING_AGG 将输入字符串按分隔符拼接为一个字符串
SUM 返回分组内所有元素的总和
TOTAL 返回分组内所有元素的总和;与 SUM 不同,全空组返回 0 而非 NULL
VARIANCE 返回分组内所有元素的方差(样本方差)

在 Rust 侧,这 16 个函数被建模为 PolarsSQLFunctions 枚举的变体,并各自带有文档注释与别名注册,见 functions.rs 中的名称映射逻辑(别名细节见下文各函数小节)。

三、FILTER (WHERE ...):只对满足条件的行做聚合

任何聚合函数调用都可以附加一个 FILTER (WHERE ...) 子句,将聚合限制在谓词为真的行上。

关键语义如下:

  • 该子句挂在单个聚合调用上,与查询外层的 WHERE 相互独立;
  • 因此同一 SELECT 中不同的聚合可以分别看到不同的行集
  • 注意:同一个聚合调用上 FILTER 不能与 OVER(窗口)同时使用

参考文档给出的标准示例(多条件聚合同一查询):

df = pl.DataFrame(
    {
        "category": ["A", "B", "A", "B", "A", "B"],
        "value":    [10, 20, 30, 40, 50, 60],
    }
)
df.sql("""
  SELECT
    category,
    SUM(value) AS total,
    SUM(value) FILTER (WHERE value > 25) AS total_high,
    SUM(value) FILTER (WHERE value <= 25) AS total_low,
    COUNT(*) FILTER (WHERE value >= 40) AS n_top
  FROM self
  GROUP BY category
  ORDER BY category
""")
# shape: (2, 5)
# ┌──────────┬───────┬────────────┬───────────┬───────┐
# │ category ┆ total ┆ total_high ┆ total_low ┆ n_top │
# │ ---      ┆ ---   ┆ ---        ┆ ---       ┆ ---   │
# │ str      ┆ i64   ┆ i64        ┆ i64       ┆ u32   │
# ╞══════════╪═══════╪════════════╪═══════════╪═══════╡
# │ A        ┆ 90    ┆ 80         ┆ 10        ┆ 1     │
# │ B        ┆ 120   ┆ 100        ┆ 20        ┆ 2     │
# └──────────┴───────┴────────────┴────────────┴───────┘

从实现看,FILTER 的处理集中在 functions.rsSQLFunctionVisitor::visit_function 会先把子句中的谓词解析为 Polars 表达式暂存,当检测到函数同时带 OVER 时直接报错 'FILTER' combined with 'OVER' is not supported;之后每个参数经 parse_sql_argfunctions.rs)解析后,都会调用 expr.filter(pred),把谓词作为局部行过滤注入该聚合的输入。这正是它只影响当前聚合、却能与全局 WHERE 并存的底层原因。相关语义在测试 py-polars/tests/unit/sql/test_filter_clause.py 中有完整覆盖,包括对 FILTEROVER 混用的报错断言。

四、描述性统计函数:AVG、MIN、MAX、MEDIAN

1. AVG

返回分组内所有元素的平均值,输出类型为 f64

df = pl.DataFrame({"bar": [20, 10, 30, 40]})
df.sql("""
  SELECT AVG(bar) AS bar_avg FROM self
""")
# shape: (1, 1)
# ┌─────────┐
# │ bar_avg │
# │ ---     │
# │ f64     │
# ╞═════════╡
# │ 25.0    │
# └─────────┘

实现上 Avg 直接调用 polars-lazy 的 .mean() 聚合(functions.rs),并复用 extract_args_distinct 支持 AVG(DISTINCT col)(在求和前先 .unique()),对 AVG(*)AVG(col) 也做了参数形态适配。SUM(DISTINCT ...)AVG(DISTINCT ...) 的组合语义可在 py-polars/tests/unit/sql/test_agg_semantics.py 中验证。

2. MIN / MAX

分别返回分组内最小/最大的元素,保持输入列的 dtype(整型输入返回整型):

df = pl.DataFrame({"bar": [20, 10, 30, 40]})
df.sql("SELECT MAX(bar) AS bar_max FROM self")
# shape: (1, 1)
# ┌─────────┐
# │ bar_max │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 40      │
# └─────────┘

df.sql("SELECT MIN(bar) AS bar_min FROM self")
# ┌─────────┐
# │ bar_min │
# │ ---     │
# │ i64     │
# ╞═════════╡
# │ 10      │
# └─────────┘

在源码中 MIN/MAX 共用 visit_min_maxfunctions.rs):普通聚合走 Expr::min/Expr::max;当带 OVER 窗口且含 ORDER BY 时,会切换到 cum_min/cum_max 累积语义实现滚动聚合。

3. MEDIAN

返回分组中位数,输出 f64

df = pl.DataFrame({"bar": [20, 10, 30, 40]})
df.sql("""
  SELECT MEDIAN(bar) AS bar_median FROM self
""")
# shape: (1, 1)
# ┌────────────┐
# │ bar_median │
# │ ---        │
# │ f64        │
# ╞════════════╡
# │ 25.0       │
# └────────────┘

[20, 10, 30, 40] 排序为 [10, 20, 30, 40],偶数个元素的中位数取中间两值平均,即 (20+30)/2 = 25.0Median 在 Rust 侧直接映射到 Expr::medianfunctions.rs)。

五、求和:SUM 与 TOTAL 的空值差异

SUM 返回分组内所有元素的总和;TOTAL 也求和,但当输入全部为 NULL 时返回 0 而非 NULL(符合 SQL 标准的是 SUM 的行为)。TOTAL 同时保留被聚合列的 dtype。二者对比如下:

df = pl.DataFrame(
    {"foo": [1, 2, 3], "bar": [None, None, None]},
    schema={"foo": pl.Int64, "bar": pl.Int64},
)
df.sql("""
  SELECT
    SUM(bar) AS bar_sum,
    TOTAL(foo) AS foo_total,
    TOTAL(bar) AS bar_total
  FROM self
""")
# shape: (1, 3)
# ┌─────────┬───────────┬───────────┐
# │ bar_sum ┆ foo_total ┆ bar_total │
# │ ---     ┆ ---       ┆ ---       │
# │ i64     ┆ i64       ┆ i64       │
# ╞═════════╪═══════════╪═══════════╡
# │ null    ┆ 6         ┆ 0         │
# └─────────┴───────────┴───────────┘

可见:bar 三值全为 NULL 时,SUM(bar) 得到 null,而 TOTAL(bar) 得到 0TOTAL 的语义在 PolarsSQLFunctions::Total 的文档注释中有明确定义(functions.rs)。

六、计数:COUNT

COUNT 返回分组内的元素个数。常见用法包括:

  • COUNT(col):统计列中非空值的数量;
  • COUNT(*):统计行数;
  • COUNT(DISTINCT col):统计去重后的不同取值数(COUNT(DISTINCT *) 亦被枚举文档列为支持形式)。
df = pl.DataFrame(
  {
    "foo": ["b", "a", "b", "c"],
    "bar": [20, 10, 30, 40]
  }
)
df.sql("""
  SELECT
    COUNT(bar) AS n_bar,
    COUNT(DISTINCT foo) AS n_foo_unique
  FROM self
""")
# shape: (1, 2)
# ┌───────┬──────────────┐
# │ n_bar ┆ n_foo_unique │
# │ ---   ┆ ---          │
# │ u32   ┆ u32          │
# ╞═══════╪══════════════╡
# │ 4     ┆ 3            │
# └───────┴──────────────┘

bar 全部非空故 n_bar = 4foo 去重后为 {b, a, c} 共 3 个,输出 dtype 为 u32。COUNT 也是与 FILTER 组合最常用的聚合之一(例如 COUNT(*) FILTER (WHERE value >= 40),见本文第三节)。

七、方差与协方差族:VARIANCE、STDDEV、COVAR、CORR

1. VARIANCE 与 STDDEV(样本估计)

VARIANCE 返回分组方差,STDDEV 返回分组标准差,二者均按样本口径计算(即分母为 n-1),输出 f64

df = pl.DataFrame(
    {
        "foo": [10, 20, 8],
        "bar": [10, 7, 18],
    }
)
df.sql("""
  SELECT
    VARIANCE(foo) AS foo_var, VARIANCE(bar) AS bar_var,
    STDDEV(foo) AS foo_std, STDDEV(bar) AS bar_std
  FROM self
""")
# shape: (1, 2)
# ┌───────────┬───────────┬──────────┬──────────┐
# │ foo_var   ┆ bar_var   ┆ foo_std  ┆ bar_std  │
# │ ---       ┆ ---       ┆ ---      ┆ ---      │
# │ f64       ┆ f64       ┆ f64      ┆ f64      │
# ╞═══════════╪═══════════╪══════════╪══════════╡
# │ 41.333333 ┆ 32.333333 ┆ 6.429101 ┆ 5.686241 │
# └───────────┴───────────┴──────────┴──────────┘

别名方面,官方文档标注:STDDEV 的别名包括 STDEVSTDEV_SAMPSTDDEV_SAMPVARIANCE 的别名包括 VARVAR_SAMP。这些别名都在 try_from_sql 的名称归一化中生效(functions.rs)。Rust 侧实现为 .std(1).var(1),其中自由度参数 1 正是样本口径(ddof = 1)的来源(functions.rs)。测试 py-polars/tests/unit/sql/test_filter_clause.pySTDDEV_SAMP/VAR_SAMP 配合 FILTER 的结果也用 math.sqrt(2.0) 做了对照。

2. COVAR(样本协方差)

COVAR 返回两列之间的样本协方差,别名 COVAR_SAMP

df = pl.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [2, 4, 7, 5, 9]})
df.sql("""
  SELECT COVAR(foo, bar) AS covar FROM self
""")
# shape: (1, 1)
# ┌───────┐
# │ covar │
# │ ---   │
# │ f64   │
# ╞═══════╡
# │ 3.75  │
# └───────┘

实现上 CovarSamp 走二元参数路径,映射到 polars_lazy::dsl::cov(a, b, 1),末尾的 1 即样本协方差;SQL 引擎同时注册了 COVAR_POP(映射 cov(a, b, 0),总体协方差),两者共同构成完整的协方差族(functions.rs)。

3. CORR(Pearson 相关系数)

CORR 返回两列间的 Pearson 相关系数,取值在 [-1, 1] 区间:

df = pl.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [2, 4, 7, 5, 9]})
df.sql("""
  SELECT CORR(foo, bar) AS corr FROM self
""")
# shape: (1, 1)
# ┌──────────┐
# │ corr     │
# │ ---      │
# │ f64      │
# ╞══════════╡
# │ 0.877809 │
# └──────────┘

Corr 在源码中通过 visit_binary(sql_corr) 完成翻译(functions.rs),其语义为两列样本标准差归一化的协方差。FILTER 测试同样覆盖了 CORR(a, b) FILTER (WHERE a > 1) 这类带条件的相关性计算。

八、分位数:QUANTILE_CONT 与 QUANTILE_DISC

两者都接受两参形式 QUANTILE_xxx(col, q),其中分位点 q 必须落在 [0, 1] 之间。差别在于取值方式:

  • QUANTILE_CONT:连续分位数,在两个最接近的分位点观测值之间线性插值得到结果;
  • QUANTILE_DISC:离散分位数,把 [0, 1] 区间切分为等长子区间并对应到各取值,返回 q 落入子区间对应的真实观测值

同一份数据对比(数据:[5, 20, 10, 30, 70, 40, 10, 90]):

df = pl.DataFrame({"foo": [5, 20, 10, 30, 70, 40, 10, 90]})
df.sql("""
  SELECT
    QUANTILE_CONT(foo, 0.25) AS foo_q25,
    QUANTILE_CONT(foo, 0.50) AS foo_q50,
    QUANTILE_CONT(foo, 0.75) AS foo_q75
  FROM self
""")
# ┌─────────┬─────────┬─────────┐
# │ foo_q25 ┆ foo_q50 ┆ foo_q75 │
# │ f64     ┆ f64     ┆ f64     │
# ╞═════════╪═════════╪═════════╡
# │ 10.0    ┆ 25.0    ┆ 47.5    │
# └─────────┴─────────┴─────────┘

df.sql("""
  SELECT
    QUANTILE_DISC(foo, 0.25) AS foo_q25,
    QUANTILE_DISC(foo, 0.50) AS foo_q50,
    QUANTILE_DISC(foo, 0.75) AS foo_q75
  FROM self
""")
# ┌─────────┬─────────┬─────────┐
# │ foo_q25 ┆ foo_q50 ┆ foo_q75 │
# │ f64     ┆ f64     ┆ f64     │
# ╞═════════╪═════════╪═════════╡
# │ 10.0    ┆ 20.0    ┆ 40.0    │
# └─────────┴─────────┴─────────┘

注意 q=0.5(中位数)这一行:CONT 得到插值的 25.0,而 DISC 返回落在子区间内的实际元素 20.0

源码实现非常直观地体现了这两种方法的差异:Rust 侧用 QuantileMethod::Linear(连续线性插值)对应 QUANTILE_CONT,用 QuantileMethod::Equiprobable(等概率)对应 QUANTILE_DISCfunctions.rs)。此外引擎会做严格校验:分位点必须是字面量且 q ∈ [0, 1],否则分别报 "QUANTILE_CONT value must be between 0 and 1""invalid value for QUANTILE_CONT" 之类的 SQL 语法错误。

九、首个与末个元素:FIRST / LAST

FIRST 返回分组内的第一个元素,LAST 返回最后一个元素,二者均保留原列 dtype(字符串列返回字符串):

df = pl.DataFrame({"foo": ["b", "a", "b", "c"]})
df.sql("SELECT FIRST(foo) AS ff FROM self")
# ┌─────┐
# │ ff  │
# │ --- │
# │ str │
# ╞═════╡
# │ b   │
# └─────┘

df.sql("SELECT LAST(foo) AS lf FROM self")
# ┌─────┐
# │ lf  │
# │ --- │
# │ str │
# ╞═════╡
# │ c   │
# └─────┘

Rust 侧 First/Last 直接映射 Expr::firstExpr::lastfunctions.rs)。若需要基于有序窗口取首尾值,可改用窗口函数 FIRST_VALUE / LAST_VALUE ... OVER (...)(见 functions.rs 的窗口函数注册)。

十、字符串拼接:STRING_AGG(别名 GROUP_CONCAT / LISTAGG)

STRING_AGG 把分组内的输入字符串拼成一个字符串,语法与能力最丰富:

  • 分隔符可选,缺省为 ","
  • 支持 DISTINCT,只拼接去重后的值;
  • 支持参数内的 ORDER BY 控制拼接顺序(可指定排序列与升降序);
  • 支持参数内的 LIMIT n 控制最多拼接多少个值;
  • 支持叠加 FILTER (WHERE ...)

官方别名:GROUP_CONCATLISTAGG

综合示例(含自定义分隔符、排序、LIMIT 与正则过滤):

df = pl.DataFrame(
    {
        "category": ["A", "B", "A", "B", "A", "B"],
        "label": ["x1", "y1", "x2", "y2", "x3", "y3"],
        "value": [10, 20, 30, 40, 50, 60],
    }
)
df.sql("""
  SELECT
    category,
    STRING_AGG(label LIMIT 2) AS two_labels,
    STRING_AGG(label, ':' ORDER BY value DESC) AS labels_desc,
    STRING_AGG(label, ',' ORDER BY value ASC) FILTER(WHERE label !~ '1$') AS labels_omit_1
  FROM self
  GROUP BY category
  ORDER BY category
""")
# shape: (2, 4)
# ┌──────────┬────────────┬─────────────┬───────────────┐
# │ category ┆ two_labels ┆ labels_desc ┆ labels_omit_1 │
# │ ---      ┆ ---        ┆ ---         ┆ ---           │
# │ str      ┆ str        ┆ str         ┆ str           │
# ╞══════════╪════════════╪═════════════╪═══════════════╡
# │ A        ┆ x1,x2      ┆ x3:x2:x1    ┆ x2,x3         │
# │ B        ┆ y1,y2      ┆ y3:y2:y1    ┆ y2,y3         │
# └──────────┴────────────┴─────────────┴───────────────┘

逐列解读:

  • two_labels:默认分隔符 ,,每组只取前 2 个值;
  • labels_desc:分隔符改为 :,且按 value 降序排列(A 组 50→30→10,故为 x3:x2:x1);
  • labels_omit_1:分隔符 ,、按 value 升序,再用 FILTER 借助正则 !~ '1$' 剔除结尾为 1 的标签(即去掉 x1/y1),得到 x2,x3y2,y3

visit_string_agg 的实现(functions.rs)展示了完整的处理管线:先把参数(表达式 + 可选分隔符)解析为表达式,随后由共享的 apply_aggregate_clauses 统一处理 DISTINCTORDER BYLIMIT 三类修饰——DISTINCT 时先 unique_stable() 再去排序,LIMIT 要求为正整数(非正整数会抛出 "LIMIT in STRING_AGG must be a positive integer"),最后把列 cast 为字符串、implode 成列表再 list().join(separator, true) 完成拼接。还有一个细节:别名 GROUP_CONCAT(来自 SQLite 方言)在同时使用 DISTINCT 与第二个分隔符参数时会被拒绝(SQLite 语义限制),而标准的 STRING_AGG/LISTAGG 形式则允许两者共存。拼接时的空值处理为:若组内存在非空值则输出拼接结果,全空组输出 NULL。

十一、函数名解析与方言兼容

从源码结构看,SQL 聚合的解析链路分三步,理解后可以自行推断更多可组合行为:

  1. 函数名归一化PolarsSQLFunctions::try_from_sql 把小写化的函数名映射到枚举变体,同一变体聚合多个方言写法,例如 stdev | stddev | stdev_samp | stddev_samp → StdDevstring_agg | listagg | group_concat → StringAggcovar_samp | covar → CovarSampvar | variance | var_samp → Variancefunctions.rs);
  2. 修饰子句校验visit_function 统一拒绝尚未支持的 WITHIN GROUPIGNORE|RESPECT NULLS 修饰,并拦截 FILTER+OVER 的组合(functions.rs);
  3. 翻译为 lazy 表达式:按变体生成 mean / sum / min / max / std(1) / var(1) / cov(...,1) / quantile(..., Linear|Equiprobable) / list.join(...) 等 polars-lazy 聚合表达式,送入查询计划参与谓词下推与向量化执行。

小结与下一步

本文以 aggregate.rst 为索引,逐一讲清了 Polars SQL 的 16 个内置聚合函数的语义、别名、空值策略与典型示例,并对照 crates/polars-sql/src/functions.rs 说明了它们在 Rust 引擎中的真实映射(包括样本 vs 总体口径的自由度参数、分位数两种插值方法、FILTER 谓词的表达式注入等)。

动手验证建议:直接复制本文各节的 Python 示例在本地运行;想进一步确认边界行为,可查阅 py-polars/tests/unit/sql/test_filter_clause.pypy-polars/tests/unit/sql/test_agg_semantics.py 中对 FILTER 聚合、DISTINCT 聚合的断言用例。聚合之外,窗口函数(SUM(...) OVER (...) 等,见 functions/window.rst)是另一个高频进阶话题——需要注意本文开头强调的限制:FILTER 的聚合不能同时带 OVER

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395