Polars SQL 窗口函数完全指南:RANK、DENSE_RANK、ROW_NUMBER、LAG、LEAD 与 OVER 窗口语义深度解析
本指南基于 Polars 仓库中的 SQL 窗口函数参考文档(window.rst),系统讲解 Polars SQL 引擎中全部窗口函数的语义、语法约束与实际用法,包括排名类函数(RANK / DENSE_RANK / ROW_NUMBER)、偏移访问函数(LAG / LEAD)与取值函数(FIRST_VALUE / LAST_VALUE),并深入剖析 OVER 子句在 Polars 中独有的 ROWS 帧语义。读完本文,你将能够在 Polars 的 df.sql() 中正确编写"分组 Top-N、同比环比、累计求和、去重打标"等典型窗口查询,并理解其与数据库引擎默认 RANGE 帧行为的本质差异。
Polars 是使用 Rust 编写的高性能 DataFrame 查询引擎,其内置 SQL 方言可覆盖下表所列的窗口函数。这些函数名在 Rust 端的 SQL 关键字注册表中均有对应登记,见 crates/polars-sql/src/functions.rs(keywords() 方法,如 "dense_rank"、"first_value"、"lag"、"last_value"、"lead"、"rank"、"row_number")。
Polars 窗口函数一览
| 函数 | 描述 |
|---|---|
DENSE_RANK |
返回每行在其窗口分区内的排名,并列值排名相同且后续排名无空洞(连续递增) |
FIRST_VALUE |
相对 OVER 声明的窗口,返回一组有序值中的第一个值 |
LAG |
返回窗口分区内当前行之前给定偏移处的列值,越界返回 NULL |
LAST_VALUE |
相对 OVER 声明的窗口,返回一组有序值中的最后一个值 |
LEAD |
返回窗口分区内当前行之后给定偏移处的列值,越界返回 NULL |
OVER |
定义一个窗口(一组行),函数在此窗口范围内被应用 |
RANK |
返回每行在其窗口分区内的排名,并列值排名相同且后续排名出现空洞(跳过编号) |
ROW_NUMBER |
返回窗口分区内的顺序行号,从 1 开始 |
其中 RANK 与 DENSE_RANK 在 Rust 实现中受 feature = "rank" 编译特性门控(见 functions.rs 中的 #[cfg(feature = "rank")]),即通过 Polars 的 Cargo feature 按需启用。
重要前提:Polars 的 ROWS 帧语义与数据库默认不同
作为 DataFrame 引擎,当窗口函数省略显式窗口规范时,Polars 默认采用 ROWS 帧语义,等价于:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
即窗口从分区第一行一直延伸到当前行。这与数据库引擎通常采用的 RANGE 帧语义(同值行归入同一 peer 组、默认扩展至分区的所有并列行)截然不同。这一差异是理解后续所有窗口函数输出结果的关键:
- 对
SUM(...) OVER (PARTITION BY ... ORDER BY ...)这类累积聚合,Polars 默认帧会在每一行处基于"分区起点 → 当前行"累计(running total); - 对
LAST_VALUE等函数,由于帧终点是CURRENT ROW,其"最后一个值"实际指当前行,这会使不指定帧的LAST_VALUE语义与直觉不同(详见下文源码剖析)。
在 Rust 实现端,帧校验逻辑位于 crates/polars-sql/src/functions.rs 的 validate_window_frame():Polars 目前只支持 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 这一种帧规范。当检测到 RANGE 或 GROUPS 单元时直接报 SQLInterface: "RANGE-based window frames are not supported" / "GROUPS-based window frames are not supported";而当出现 N PRECEDING、N FOLLOWING 等其他 ROWS 边界时,则会提示 "only 'ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW' is currently supported"。
OVER:定义函数作用的窗口
OVER 用于定义一个窗口(一组行),窗口函数在该组行内被应用。窗口规范一般包含三个组成部分:
PARTITION BY col—— 按列分区,函数在各自分区内独立计算;ORDER BY col [ASC|DESC]—— 定义分区内的行顺序;- 显式帧规范(frame)—— 进一步圈定参与计算的行的范围(Polars 目前仅支持默认的
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)。
一个同时展示 FIRST_VALUE、LAST_VALUE 与 SUM 累积求和三种语义的完整示例:
df = pl.DataFrame(
{
"idx": [0, 1, 2, 3, 4, 5, 6],
"label": ["aaa", "aaa", "bbb", "bbb", "aaa", "ccc", "aaa"],
"value": [10, 20, 30, 40, 50, -5, 0],
}
)
df.sql("""
SELECT
*,
FIRST_VALUE(value) OVER (
PARTITION BY label ORDER BY idx
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS first_val,
LAST_VALUE(value) OVER (
PARTITION BY label ORDER BY idx
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS last_val,
SUM(value) OVER (
PARTITION BY label ORDER BY idx
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total_by_label
FROM self
ORDER BY label, idx
""")
# shape: (7, 6)
# ┌─────┬───────┬───────┬───────────┬──────────┬────────────────────────┐
# │ idx ┆ label ┆ value ┆ first_val ┆ last_val ┆ running_total_by_label │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ i64 ┆ str ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
# ╞═════╪═══════╪═══════╪═══════════╪══════════╪════════════════════════╡
# │ 0 ┆ aaa ┆ 10 ┆ 10 ┆ 10 ┆ 10 │
# │ 1 ┆ aaa ┆ 20 ┆ 10 ┆ 20 ┆ 30 │
# │ 4 ┆ aaa ┆ 50 ┆ 10 ┆ 50 ┆ 80 │
# │ 6 ┆ aaa ┆ 0 ┆ 10 ┆ 0 ┆ 80 │
# │ 2 ┆ bbb ┆ 30 ┆ 30 ┆ 30 ┆ 30 │
# │ 3 ┆ bbb ┆ 40 ┆ 30 ┆ 40 ┆ 70 │
# │ 5 ┆ ccc ┆ -5 ┆ -5 ┆ -5 ┆ -5 │
# └─────┴───────┴───────┴───────────┴──────────┴────────────────────────┘
从这个输出可以直观观察到:
first_val在aaa分区始终为第一个值10(帧起点固定为分区开头);last_val随当前行移动而变化,本质是"截至当前行的最后一个值",这正是ROWS ... AND CURRENT ROW帧的直接体现;running_total_by_label是按行递增的累积和(如aaa分区依次为 10、30、80、80),是典型的"运行总计"(running total)计算方式。
另外,OVER 后也可不带任何分区与排序(OVER ()),表示整个结果集作为一个窗口,可用于计算全局计数/总和。Polars 的窗口聚合测试覆盖了这种用法,参见 py-polars/tests/unit/sql/test_window_functions.py 中的 test_window_function_over_empty(COUNT(*) OVER () AS total_count)以及 OVER 与 PARTITION BY 多列组合(test_window_function_partition_by_multi)等场景。
排名类函数:RANK、DENSE_RANK 与 ROW_NUMBER
三个排名函数都要求配合 OVER 使用,但彼此的约束与语义差异明显:
RANK:分区内并列值排名相同,后续名次跳过产生空洞(如 1、1、3);DENSE_RANK:分区内并列值排名相同,但后续名次连续(如 1、1、2);ROW_NUMBER:即便值并列也总是返回唯一编号,从 1 开始连续编号。
从源码实现看,RANK 与 DENSE_RANK 均要求 OVER 子句中带有 ORDER BY(crates/polars-sql/src/functions.rs),否则抛出 SQLSyntax 错误:"RANK requires an OVER clause with ORDER BY"(DENSE_RANK 同理);这两个函数不接受任何参数。RANK 映射到 RankMethod::Min,DENSE_RANK 映射到 RankMethod::Dense,并提取窗口规范中的排序表达式构造底层 Expr::rank。若 ORDER BY 后有多列,则先组成结构列再统一求秩,排序方向由窗口规范统一解析。
RANK 与 DENSE_RANK 的直观对比
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5, 6],
"category": ["A", "A", "A", "B", "B", "B"],
"score": [85, 90, 90, 75, 80, 80]
})
df.sql("""
SELECT
id,
category,
score,
RANK() OVER (PARTITION BY category ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY category ORDER BY score DESC) AS dense_rank
FROM self
ORDER BY category, score DESC
""")
# shape: (6, 5)
# ┌─────┬──────────┬───────┬──────┬────────────┐
# │ id ┆ category ┆ score ┆ rank ┆ dense_rank │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ i64 ┆ str ┆ i64 ┆ u32 ┆ u32 │
# ╞═════╪══════════╪═══════╪══════╪════════════╡
# │ 2 ┆ A ┆ 90 ┆ 1 ┆ 1 │
# │ 3 ┆ A ┆ 90 ┆ 1 ┆ 1 │
# │ 1 ┆ A ┆ 85 ┆ 3 ┆ 2 │
# │ 5 ┆ B ┆ 80 ┆ 1 ┆ 1 │
# │ 6 ┆ B ┆ 80 ┆ 1 ┆ 1 │
# │ 4 ┆ B ┆ 75 ┆ 3 ┆ 2 │
# └─────┴──────────┴───────┴──────┴────────────┘
以 category = 'A' 分区为例:两条 90 分并列占据名次 1;85 分在 RANK 下是第 3 名(跳过 2),而在 DENSE_RANK 下是第 2 名。两列输出类型均为 u32。
ROW_NUMBER:始终唯一的顺序号
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5, 6],
"category": ["A", "A", "A", "B", "B", "B"],
"value": [100, 200, 200, 150, 300, 150]
})
df.sql("""
SELECT
ROW_NUMBER() AS x,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY id) AS y,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY id DESC) AS z,
category,
value
FROM self
ORDER BY category, id
""")
# shape: (6, 5)
# ┌─────┬─────┬─────┬──────────┬───────┐
# │ x ┆ y ┆ z ┆ category ┆ value │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ u32 ┆ u32 ┆ u32 ┆ str ┆ i64 │
# ╞═════╪═════╪═════╪══════════╪═══════╡
# │ 1 ┆ 1 ┆ 3 ┆ A ┆ 100 │
# │ 2 ┆ 2 ┆ 2 ┆ A ┆ 200 │
# │ 3 ┆ 3 ┆ 1 ┆ A ┆ 200 │
# │ 4 ┆ 1 ┆ 3 ┆ B ┆ 150 │
# │ 5 ┆ 2 ┆ 2 ┆ B ┆ 300 │
# │ 6 ┆ 3 ┆ 1 ┆ B ┆ 150 │
# └─────┴─────┴─────┴──────────┴───────┘
此例同时演示了三种形态:
x = ROW_NUMBER()(无OVER)在整个结果集上从 1 顺序编号(1 到 6);y按category分区、id升序,各分区内从 1 重新开始(A 区 1、2、3;B 区 1、2、3);z按id降序编号,因此id最大者编号为 1,同一分区内与y恰好逆序。
源码实现上,ROW_NUMBER 不允许带参数,其表达式为 int_range(lit(0), len(), 1, UInt32) + 1(源码注释明确指出 "SQL is 1-indexed",见 crates/polars-sql/src/functions.rs),结果类型为 u32。与 RANK/DENSE_RANK 不同,ROW_NUMBER 不强制要求 ORDER BY——从 py-polars/tests/unit/sql/test_rank_functions.py 的 test_rank_funcs_require_order_by 可见,RANK() OVER (PARTITION BY category)(无 ORDER BY)会报错,而无排序的 ROW_NUMBER() 是合法的(按引擎内部顺序编号)。排名函数在分区下的等价行为也有专门的对比测试(同文件 test_rank_funcs_with_partition 等),可供参考验证。
偏移访问函数:LAG 与 LEAD
LAG(expr[, n]) 返回当前行之前第 n 行的列值,LEAD(expr[, n]) 返回当前行之后第 n 行的列值;当偏移越出分区边界时返回 NULL。
语法:
LAG(expr) OVER (...):偏移量默认为 1;LAG(expr, n) OVER (...):n行偏移;LEAD(expr)/LEAD(expr, n)同构。
要求:
- 必须配合
OVER子句使用; OVER子句中必须包含ORDER BY。
以 LAG 为例(LEAD 行为完全对称):
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5, 6],
"category": ["A", "A", "A", "B", "B", "B"],
"value": [10, 20, 30, 40, 50, 60],
})
df.sql("""
SELECT
id,
category,
value,
LAG(value) OVER (PARTITION BY category ORDER BY id) AS prev_value,
LAG(value, 2) OVER (PARTITION BY category ORDER BY id) AS prev2_value
FROM self
ORDER BY category, id
""")
# shape: (6, 5)
# ┌─────┬──────────┬───────┬────────────┬─────────────┐
# │ id ┆ category ┆ value ┆ prev_value ┆ prev2_value │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ i64 ┆ str ┆ i64 ┆ i64 ┆ i64 │
# ╞═════╪══════════╪═══════╪════════════╪═════════════╡
# │ 1 ┆ A ┆ 10 ┆ null ┆ null │
# │ 2 ┆ A ┆ 20 ┆ 10 ┆ null │
# │ 3 ┆ A ┆ 30 ┆ 20 ┆ 10 │
# │ 4 ┆ B ┆ 40 ┆ null ┆ null │
# │ 5 ┆ B ┆ 50 ┆ 40 ┆ null │
# │ 6 ┆ B ┆ 60 ┆ 50 ┆ 40 │
# └─────┴──────────┴───────┴────────────┴─────────────┘
LEAD 的对应输出请见 window.rst 原文示例:分区 A 中 LEAD(value) 依次返回 20、30、NULL,而 LEAD(value, 2) 依次返回 30、NULL、NULL。
底层实现中,LAG 与 LEAD 共用同一个辅助方法 visit_window_offset_function(见 crates/polars-sql/src/functions.rs),区别仅在于传入的乘数:LAG 为 +1(向过去偏移,底层调用 expr.shift(n)),LEAD 为 -1(向未来偏移,底层 expr.shift(-n))。该实现还包含两层校验:
OVER子句缺失 → 报SQLSyntax: "lag requires an OVER clause";ORDER BY缺失 → 报SQLSyntax: "lag requires an ORDER BY in the OVER clause";- 显式偏移量必须为正整数,若传入
0或负数会报"offset must be positive",非整数字面量报"offset must be an integer"。
取值函数:FIRST_VALUE 与 LAST_VALUE
FIRST_VALUE(expr) 返回相对 OVER 窗口而言有序值集合中的第一个值;LAST_VALUE(expr) 返回最后一个值。
# FIRST_VALUE 示例
df.sql("""
SELECT FIRST_VALUE(col1) OVER (PARTITION BY category ORDER BY id) FROM df;
""")
# LAST_VALUE 示例
df.sql("""
SELECT LAST_VALUE(col1) OVER (PARTITION BY category ORDER BY id) FROM df;
""")
值得特别注意的是 LAST_VALUE 在 Polars 中的实现细节:Rust 端注释明确指出——由于默认窗口帧是 ROWS ... UNBOUNDED PRECEDING AND CURRENT ROW,LAST_VALUE 返回"从分区起点截至当前行的最后一个值",在默认帧下恰好就是当前行的值。因此源码中 LastValue 分支并未套用窗口表达式,而是直接把参数表达式解析为普通列表达式(并校验参数个数必须为 1,见 crates/polars-sql/src/functions.rs)。FIRST_VALUE 则映射为 Expr::first 并应用完整窗口规范。
想要得到"分区内真正的最后一个值",应配合显式的 ROWS 帧(如上文 OVER 示例中的 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 并不会改变 LAST_VALUE 的默认语义——它是累积到当前行的最后值),或结合 ORDER BY 语义小心设计查询;这正是理解默认帧语义最有价值的应用场景。FIRST_VALUE/LAST_VALUE 对单个标量表达式求值,示例数据中的语义可在上文 OVER 一节直接对照验证。
源码视角:窗口函数是如何从 SQL 落到表达式的
Polars SQL 引擎处理窗口查询的完整链路大致如下:
- 注册:所有窗口函数名在
PolarsSQLFunctions::keywords()中登记(见 crates/polars-sql/src/functions.rs),其中rank/dense_rank在#[cfg(feature = "rank")]下启用; - 解析:SQL 解析阶段把
RANK、DENSE_RANK、LAG、LEAD、ROW_NUMBER、FIRST_VALUE、LAST_VALUE等标识符映射为PolarsSQLFunctions枚举变体,并携带over窗口规范(WindowType/WindowSpec,含partition_by、order_by与帧信息); - 翻译:在
visit_*系列方法中,把窗口函数翻译为 Polars 惰性表达式树——Rank/DenseRank构造Expr::rank(RankOptions, None)后经apply_window_spec套上窗口;RowNumber用int_range生成 1 起始序列;Lag/Lead走visit_window_offset_function后同样apply_window_spec; - 校验:
validate_window_frame兜底检查帧合法性,杜绝不支持的RANGE/GROUPS与越界ROWS帧进入执行阶段; - 执行:查询整体由
SQLContext::execute入口驱动(见 crates/polars-sql/src/context.rs),翻译得到LazyFrame后交给 Polars 惰性/流式执行引擎完成计算。
工程验证与常见限制总结
围绕窗口函数,仓库提供了两类测试保障:
- 排名函数专项测试 py-polars/tests/unit/sql/test_rank_functions.py:覆盖
RANK/DENSE_RANK对比(1、1、3 vs 1、1、2)、分区排名、DESC多列排序排名、以及"无ORDER BY时报错"的约束; - 窗口函数专项测试 py-polars/tests/unit/sql/test_window_functions.py:覆盖
OVER (ORDER BY ...)累积聚合、PARTITION BY+ 窗口别名(OVER w0)复用、OVER ()全局窗口、ASC/DESC方向差异、多列分区/排序、NULL 值参与聚合等场景。
使用时的常见限制(均已由源码校验逻辑证实):
| 函数 | 是否必须有 OVER |
是否必须有 ORDER BY |
参数约束 |
|---|---|---|---|
RANK / DENSE_RANK |
是 | 是 | 不允许参数 |
ROW_NUMBER |
否(可全表编号) | 否(可用任意顺序) | 不允许参数 |
LAG / LEAD |
是 | 是 | 1 个表达式 + 可选正整数偏移(默认 1) |
FIRST_VALUE / LAST_VALUE |
是 | 依语义需要 | 恰好 1 个参数 |
| 帧规范 | 只能是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,RANGE/GROUPS/其它 ROWS 边界均不支持 |
把握住"Polars 默认 ROWS 帧、等价于累积到当前行"这一核心差异,再结合各类函数的参数约束,你就能在 df.sql() 中写出既符合直觉又经引擎优化的窗口查询,并与 Polars 表达式 API 中的 rank、shift、cum_sum 等能力相互印证、按需混用。
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
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
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