Polars SQL 聚合函数权威指南:AVG、SUM、COUNT 到 QUANTILE、STRING_AGG 的 16 个内置聚合全解析
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 BY、HAVING、ORDER 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.rs:SQLFunctionVisitor::visit_function 会先把子句中的谓词解析为 Polars 表达式暂存,当检测到函数同时带 OVER 时直接报错 'FILTER' combined with 'OVER' is not supported;之后每个参数经 parse_sql_arg(functions.rs)解析后,都会调用 expr.filter(pred),把谓词作为局部行过滤注入该聚合的输入。这正是它只影响当前聚合、却能与全局 WHERE 并存的底层原因。相关语义在测试 py-polars/tests/unit/sql/test_filter_clause.py 中有完整覆盖,包括对 FILTER 与 OVER 混用的报错断言。
四、描述性统计函数: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_max(functions.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.0。Median 在 Rust 侧直接映射到 Expr::median(functions.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) 得到 0;TOTAL 的语义在 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 = 4,foo 去重后为 {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 的别名包括 STDEV、STDEV_SAMP、STDDEV_SAMP;VARIANCE 的别名包括 VAR、VAR_SAMP。这些别名都在 try_from_sql 的名称归一化中生效(functions.rs)。Rust 侧实现为 .std(1) 与 .var(1),其中自由度参数 1 正是样本口径(ddof = 1)的来源(functions.rs)。测试 py-polars/tests/unit/sql/test_filter_clause.py 中 STDDEV_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_DISC(functions.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::first 与 Expr::last(functions.rs)。若需要基于有序窗口取首尾值,可改用窗口函数 FIRST_VALUE / LAST_VALUE ... OVER (...)(见 functions.rs 的窗口函数注册)。
十、字符串拼接:STRING_AGG(别名 GROUP_CONCAT / LISTAGG)
STRING_AGG 把分组内的输入字符串拼成一个字符串,语法与能力最丰富:
- 分隔符可选,缺省为
","; - 支持
DISTINCT,只拼接去重后的值; - 支持参数内的
ORDER BY控制拼接顺序(可指定排序列与升降序); - 支持参数内的
LIMIT n控制最多拼接多少个值; - 支持叠加
FILTER (WHERE ...)。
官方别名:GROUP_CONCAT、LISTAGG。
综合示例(含自定义分隔符、排序、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,x3与y2,y3。
visit_string_agg 的实现(functions.rs)展示了完整的处理管线:先把参数(表达式 + 可选分隔符)解析为表达式,随后由共享的 apply_aggregate_clauses 统一处理 DISTINCT、ORDER BY、LIMIT 三类修饰——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 聚合的解析链路分三步,理解后可以自行推断更多可组合行为:
- 函数名归一化:
PolarsSQLFunctions::try_from_sql把小写化的函数名映射到枚举变体,同一变体聚合多个方言写法,例如stdev | stddev | stdev_samp | stddev_samp → StdDev、string_agg | listagg | group_concat → StringAgg、covar_samp | covar → CovarSamp、var | variance | var_samp → Variance(functions.rs); - 修饰子句校验:
visit_function统一拒绝尚未支持的WITHIN GROUP与IGNORE|RESPECT NULLS修饰,并拦截FILTER+OVER的组合(functions.rs); - 翻译为 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.py 与 py-polars/tests/unit/sql/test_agg_semantics.py 中对 FILTER 聚合、DISTINCT 聚合的断言用例。聚合之外,窗口函数(SUM(...) OVER (...) 等,见 functions/window.rst)是另一个高频进阶话题——需要注意本文开头强调的限制:带 FILTER 的聚合不能同时带 OVER。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00