首页
/ Polars SQL 窗口函数完全指南:RANK、DENSE_RANK、ROW_NUMBER、LAG、LEAD 与 OVER 窗口语义深度解析

Polars SQL 窗口函数完全指南:RANK、DENSE_RANK、ROW_NUMBER、LAG、LEAD 与 OVER 窗口语义深度解析

2026-09-08 15:01:35作者:翟江哲Frasier

本指南基于 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.rskeywords() 方法,如 "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 开始

其中 RANKDENSE_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.rsvalidate_window_frame():Polars 目前只支持 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 这一种帧规范。当检测到 RANGEGROUPS 单元时直接报 SQLInterface: "RANGE-based window frames are not supported" / "GROUPS-based window frames are not supported";而当出现 N PRECEDINGN FOLLOWING 等其他 ROWS 边界时,则会提示 "only 'ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW' is currently supported"

OVER:定义函数作用的窗口

OVER 用于定义一个窗口(一组行),窗口函数在该组行内被应用。窗口规范一般包含三个组成部分:

  1. PARTITION BY col —— 按列分区,函数在各自分区内独立计算;
  2. ORDER BY col [ASC|DESC] —— 定义分区内的行顺序;
  3. 显式帧规范(frame)—— 进一步圈定参与计算的行的范围(Polars 目前仅支持默认的 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)。

一个同时展示 FIRST_VALUELAST_VALUESUM 累积求和三种语义的完整示例:

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_valaaa 分区始终为第一个值 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_emptyCOUNT(*) OVER () AS total_count)以及 OVERPARTITION 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 开始连续编号。

从源码实现看,RANKDENSE_RANK 均要求 OVER 子句中带有 ORDER BYcrates/polars-sql/src/functions.rs),否则抛出 SQLSyntax 错误:"RANK requires an OVER clause with ORDER BY"(DENSE_RANK 同理);这两个函数不接受任何参数。RANK 映射到 RankMethod::MinDENSE_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);
  • ycategory 分区、id 升序,各分区内从 1 重新开始(A 区 1、2、3;B 区 1、2、3);
  • zid 降序编号,因此 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.pytest_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、NULLNULL

底层实现中,LAGLEAD 共用同一个辅助方法 visit_window_offset_function(见 crates/polars-sql/src/functions.rs),区别仅在于传入的乘数:LAG+1(向过去偏移,底层调用 expr.shift(n)),LEAD-1(向未来偏移,底层 expr.shift(-n))。该实现还包含两层校验:

  1. OVER 子句缺失 → 报 SQLSyntax: "lag requires an OVER clause"
  2. ORDER BY 缺失 → 报 SQLSyntax: "lag requires an ORDER BY in the OVER clause"
  3. 显式偏移量必须为正整数,若传入 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 ROWLAST_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 引擎处理窗口查询的完整链路大致如下:

  1. 注册:所有窗口函数名在 PolarsSQLFunctions::keywords() 中登记(见 crates/polars-sql/src/functions.rs),其中 rank/dense_rank#[cfg(feature = "rank")] 下启用;
  2. 解析:SQL 解析阶段把 RANKDENSE_RANKLAGLEADROW_NUMBERFIRST_VALUELAST_VALUE 等标识符映射为 PolarsSQLFunctions 枚举变体,并携带 over 窗口规范(WindowType / WindowSpec,含 partition_byorder_by 与帧信息);
  3. 翻译:在 visit_* 系列方法中,把窗口函数翻译为 Polars 惰性表达式树——Rank/DenseRank 构造 Expr::rank(RankOptions, None) 后经 apply_window_spec 套上窗口;RowNumberint_range 生成 1 起始序列;Lag/Leadvisit_window_offset_function 后同样 apply_window_spec
  4. 校验validate_window_frame 兜底检查帧合法性,杜绝不支持的 RANGE/GROUPS 与越界 ROWS 帧进入执行阶段;
  5. 执行:查询整体由 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 ROWRANGE/GROUPS/其它 ROWS 边界均不支持

把握住"Polars 默认 ROWS 帧、等价于累积到当前行"这一核心差异,再结合各类函数的参数约束,你就能在 df.sql() 中写出既符合直觉又经引擎优化的窗口查询,并与 Polars 表达式 API 中的 rankshiftcum_sum 等能力相互印证、按需混用。

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

项目优选

收起
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
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
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
393