📑 本页目录(点开跳转)
07 · 窗口函数:不塌行的聚合
⏱ 70 分钟 | ⭐ GROUP BY 把十行压成一行;窗口函数让十行还是十行,但每一行都多知道一件关于自己那一组的事
🎯 一句话
OVER (…) 是「给这一行开一扇窗,看看窗里那些行」——算完之后行还在。
所有需要「这一行 vs 它所在的组」的问题(排第几、比上一次涨了多少、占本组多少),都是这一个语法。
🕳️ 一、这一章的地基,站内一块都没有
⭐ 这是本板块最硬的一条立项理由,值得先说清楚。
全站 15 个板块、392 页正文里,窗口函数 / PARTITION BY / ROW_NUMBER / RANK() /
DENSE_RANK / LAG( / LEAD( —— 命中数全部是 0。
唯一一处 OVER ( 出现在 模型上线之后 · 04 训练推理一致性:
那里把「训练侧 Spark SQL 写的 AVG(price) OVER (last 7 days)」和「线上 Java 服务写的 sum/count 最近7天」
并排放着,用来说明两套实现看起来一样、其实对不齐。
⭐ 那是一段有意写成示意的对照伪代码,不是一条可跑的查询 —— 它要表达的东西也不需要可跑。
结论不是「站内写错了」,而是「站内从来没有真正讲过窗口函数」。
⚠️ 而至少三处的正题都在等它:
| 站内哪里 | 它要什么 |
|---|---|
| 推荐算法 07 特征工程 | 「统计特征要同时喂 ctr_1h / ctr_1d / ctr_7d / ctr_30d 多个时间窗口」—— 只给结论不给算法 |
| 数据这一关 14 数据泄漏的七种来源 | 「最隐蔽的一种时间穿越:SQL 对每一行样本用的都是同一个固定窗口,正确写法必须让窗口跟着样本时间走」 |
| 数据这一关 18 特征存储 | point-in-time:「对每一行训练样本,取该样本事件时间之前最后一次可见的特征值」 |
这三条要的都是「每一行有自己的窗口」,那正是 OVER 的定义。
本章讲窗口函数本身,⭐ 窗口的边界怎么划(帧)整个让给下一章。
🧩 二、三件套:OVER / PARTITION BY / ORDER BY
语法只有一个形状:
函数(参数) OVER ( PARTITION BY 分组列 ORDER BY 排序列 )
三个部件各管一件事,都可以省略:
| 部件 | 管什么 | 省略了会怎样 |
|---|---|---|
OVER (…) |
声明「这是个窗口函数」 | 没有它 SUM(x) 就是普通聚合,会塌行 |
PARTITION BY |
把行分成若干组,每组各开各的窗 | 整张表算一组 |
ORDER BY |
组内的顺序 | 排名类函数会失去意义;⚠️ 聚合类函数的默认帧会变,这是下一章的正题 |
⚠️ 窗口里的 ORDER BY 和查询末尾的 ORDER BY 是两个东西:前者决定「窗里怎么排」,后者决定「结果怎么给你」。两者可以完全不同。
📊 三、和 GROUP BY 的根本差别:塌不塌行
同一张表、同一个 AVG,两种写法:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE runs (model TEXT, dt TEXT, auc REAL)")
db.executemany("INSERT INTO runs VALUES (?,?,?)", [
("rank_v3", "2026-03-01", 0.812), ("rank_v3", "2026-03-08", 0.831),
("rank_v3", "2026-03-15", 0.826), ("rank_v3", "2026-03-22", 0.831),
("recall_v1", "2026-03-01", 0.774), ("recall_v1", "2026-03-08", 0.769),
("recall_v1", "2026-03-15", 0.802),
])
print("原表行数 :", db.execute("SELECT COUNT(*) FROM runs").fetchone()[0])
print("\n-- GROUP BY:七行塌成两行 --")
for r in db.execute("""
SELECT model, COUNT(*) AS n, ROUND(AVG(auc), 4) AS avg_auc
FROM runs GROUP BY model ORDER BY model"""):
print(" ", r)
print("\n-- 窗口函数:七行还是七行,每行多知道自己组里的情况 --")
for r in db.execute("""
SELECT model, dt, auc,
COUNT(*) OVER (PARTITION BY model) AS n,
ROUND(AVG(auc) OVER (PARTITION BY model), 4) AS avg_auc,
ROUND(auc - AVG(auc) OVER (PARTITION BY model), 4) AS diff
FROM runs ORDER BY model, dt"""):
print(" ", r)
实测输出:
对照
原表行数 : 7
-- GROUP BY:七行塌成两行 --
('rank_v3', 4, 0.825)
('recall_v1', 3, 0.7817)
-- 窗口函数:七行还是七行,每行多知道自己组里的情况 --
('rank_v3', '2026-03-01', 0.812, 4, 0.825, -0.013)
('rank_v3', '2026-03-08', 0.831, 4, 0.825, 0.006)
('rank_v3', '2026-03-15', 0.826, 4, 0.825, 0.001)
('rank_v3', '2026-03-22', 0.831, 4, 0.825, 0.006)
('recall_v1', '2026-03-01', 0.774, 3, 0.7817, -0.0077)
('recall_v1', '2026-03-08', 0.769, 3, 0.7817, -0.0127)
('recall_v1', '2026-03-15', 0.802, 3, 0.7817, 0.0203)
⭐ 注意最后那一列 diff:它同时用到了「这一行的 auc」和「这一组的均值」。
GROUP BY 写不出这一列 —— 聚合完之后,明细行已经不存在了,你没有 auc 可以减。
GROUP BY |
窗口函数 | |
|---|---|---|
| 输出行数 | 每组一行(7 → 2) | 和输入一样(7 → 7) |
| 还能看到明细吗 | ❌ 没了 | ✅ 在 |
| 典型问题 | 「每个模型平均多少」 | ⭐ 「这一次比本模型平均高多少 / 排第几 / 占多少」 |
| 想要两者 | 要么 GROUP BY 之后再连回明细表,要么…… |
⭐ 一句 OVER (PARTITION BY …) |
⭐ 判据一句话:问题里出现「这一行 vs 它那一组」,就是窗口函数。 出现「每组一行的汇总」,就是
GROUP BY(那是 03 章)。
🥇 四、排名三兄弟:区别全在并列上
不并列的时候三个函数结果完全一样,所以必须拿有并列值的数据来看:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE runs (model TEXT, dt TEXT, auc REAL)")
db.executemany("INSERT INTO runs VALUES (?,?,?)", [
("rank_v3", "2026-03-01", 0.812), ("rank_v3", "2026-03-08", 0.831),
("rank_v3", "2026-03-15", 0.826), ("rank_v3", "2026-03-22", 0.831), # ⭐ 0.831 出现两次
])
print("dt auc ROW_NUMBER RANK DENSE_RANK")
for r in db.execute("""
SELECT dt, auc,
ROW_NUMBER() OVER (ORDER BY auc DESC) AS rn,
RANK() OVER (ORDER BY auc DESC) AS rk,
DENSE_RANK() OVER (ORDER BY auc DESC) AS dk
FROM runs ORDER BY rn"""):
print(f"{r[0]} {r[1]:.3f} {r[2]} {r[3]} {r[4]}")
print("\n三者各自的最大值 :", db.execute("""
SELECT MAX(rn), MAX(rk), MAX(dk) FROM (
SELECT ROW_NUMBER() OVER (ORDER BY auc DESC) rn,
RANK() OVER (ORDER BY auc DESC) rk,
DENSE_RANK() OVER (ORDER BY auc DESC) dk
FROM runs)""").fetchone())
print("\n⭐ 加一个唯一的决胜列,结果就定死了:")
for r in db.execute("""
SELECT dt, ROW_NUMBER() OVER (ORDER BY auc DESC, dt DESC) rn FROM runs ORDER BY rn"""):
print(" ", r)
实测输出:
并列时,三个排名函数给出的序号不同
| 日期 | AUC | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| 2026-03-08 | 0.831 | 1 | 1 | 1 |
| 2026-03-22 | 0.831 | 2 | 1 | 1 |
| 2026-03-15 | 0.826 | 3 | 3 | 2 |
| 2026-03-01 | 0.812 | 4 | 4 | 3 |
加一个唯一的决胜列后,顺序才被固定:2026-03-22、03-08、03-15、03-01。
| 函数 | 并列时 | 之后 | 最大值 | ⭐ 什么时候用 |
|---|---|---|---|---|
ROW_NUMBER() |
强行分先后(1, 2) | 正常 +1 | 4 | 去重、每组取一条:它保证「每组恰好一个 1」 |
RANK() |
同名次(1, 1) | ⚠️ 跳号(下一个是 3) | 4 | 「并列第一有两个」这种真实名次;跳号是特性不是 bug |
DENSE_RANK() |
同名次(1, 1) | 不跳号(下一个是 2) | 3 | 「一共有几档」—— ⭐ 它的最大值等于不同取值的个数 |
⚠️ ROW_NUMBER() 在并列时把谁排前面,SQL 语义不做承诺。
上面 ORDER BY auc DESC 里,两条 0.831 谁拿 1 号是数据库自己定的 ——
本机连跑两次都给了 03-08,但这只是本机这一版的行为,不是保证。
⭐ 解法和 05 章的复合游标是同一件事:排序列末尾补一个唯一列当决胜。
加上 , dt DESC 之后,1 号变成 03-22 并且永远是它。
💀 这个坑最贵的形态是「每组取最新一条」的去重: 用
ROW_NUMBER() … ORDER BY updated_at DESC取rn = 1, 如果updated_at撞车(AI全栈 06b 实证过:批量导入、脚本回填一秒能写几百条), 你每次跑同一条 SQL 可能拿到不同的那一条 —— 而且它不报错,两次结果都"看起来对"。
🛑 读到这里可以停 —— 前半章讲完了(约 25 分钟)。 后半章还有:
LAG/LEAD:和上一行、下一行比 · 窗口函数不能写在WHERE里 · 你其实已经在用「帧」了 回来的时候不用重读,直接从下一节接着看就行。
↔️ 五、LAG / LEAD:和上一行、下一行比
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE runs (model TEXT, dt TEXT, auc REAL)")
db.executemany("INSERT INTO runs VALUES (?,?,?)", [
("rank_v3", "2026-03-01", 0.812), ("rank_v3", "2026-03-08", 0.831),
("rank_v3", "2026-03-15", 0.826), ("rank_v3", "2026-03-22", 0.831),
("recall_v1", "2026-03-01", 0.774), ("recall_v1", "2026-03-08", 0.769),
("recall_v1", "2026-03-15", 0.802),
])
q = """
SELECT model, dt, auc,
LAG(auc) OVER (PARTITION BY model ORDER BY dt) AS prev_auc,
ROUND(auc - LAG(auc) OVER (PARTITION BY model ORDER BY dt), 4) AS delta
FROM runs ORDER BY model, dt
"""
for r in db.execute(q):
print(" ", r)
print("\n⭐ 只看比上一次掉了的那几次重训:")
for r in db.execute(f"WITH x AS ({q}) SELECT model, dt, delta FROM x WHERE delta < 0"):
print(" ", r)
print("\n每个 model 第一行的 prev_auc :",
db.execute(f"SELECT COUNT(*) FROM ({q}) WHERE prev_auc IS NULL").fetchone()[0], "个 NULL")
print("\nLAG 的第三个参数可以给默认值(第一行不再是 NULL):")
for r in db.execute("""
SELECT model, dt, LAG(auc, 1, auc) OVER (PARTITION BY model ORDER BY dt) AS prev_or_self
FROM runs ORDER BY model, dt LIMIT 3"""):
print(" ", r)
实测输出:
| model | 日期 | auc | prev_auc | 变化 |
|---|---|---|---|---|
| rank_v3 | 2026-03-01 | 0.812 | NULL | NULL |
| rank_v3 | 2026-03-08 | 0.831 | 0.812 | 0.019 |
| rank_v3 | 2026-03-15 | 0.826 | 0.831 | -0.005 |
| rank_v3 | 2026-03-22 | 0.831 | 0.826 | 0.005 |
| recall_v1 | 2026-03-01 | 0.774 | NULL | NULL |
| recall_v1 | 2026-03-08 | 0.769 | 0.774 | -0.005 |
| recall_v1 | 2026-03-15 | 0.802 | 0.769 | 0.033 |
⭐ 只看比上一次掉了的那几次重训:
| model | 日期 | 下降量 |
|---|---|---|
| rank_v3 | 2026-03-15 | -0.005 |
| recall_v1 | 2026-03-08 | -0.005 |
每个 model 第一行的 prev_auc : 2 个 NULL
LAG 的第三个参数可以给默认值(第一行不再是 NULL):
| model | 日期 | 带默认值的 prev_auc |
|---|---|---|
| rank_v3 | 2026-03-01 | 0.812 |
| rank_v3 | 2026-03-08 | 0.812 |
| rank_v3 | 2026-03-15 | 0.831 |
三件事:
- ⭐
LAG(x)取的是「同一个分区里、按窗口ORDER BY排在前一行」的x。 换成LEAD就是后一行。 每个分区各自从头开始 —— 所以recall_v1的第一行不会去拿rank_v3的最后一行。 - ⚠️ 每个分区的第一行必然是
NULL(实测 2 个)。于是WHERE delta < 0会把这两行静默丢掉 ——NULL < 0的结果不是「假」,是「不知道」,而WHERE只放行「真」(这是 04 章的正题)。 ⭐ 这次丢掉是对的(第一次重训没有"上一次"可比),但你必须是知道了才让它丢。 LAG(auc, 1, auc)的第三个参数是取不到时的默认值。上面拿「自己」当默认值,于是第一行的环比是 0 而不是NULL。 ⚠️ 这不总是更好:把"没有上一次"伪装成"没有变化",在做告警时是危险的。
🚦 六、窗口函数不能写在 WHERE 里
这是初学窗口函数百分之百会撞的一堵墙,而它的解释就是 01 章那张求值顺序图:
结果对照
⭐ 窗口函数是在 WHERE / GROUP BY / HAVING 全部做完之后才求值的。
WHERE 执行的那一刻,ROW_NUMBER() 还不存在,所以它引用不了。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE runs (model TEXT, dt TEXT, auc REAL)")
db.executemany("INSERT INTO runs VALUES (?,?,?)", [
("rank_v3", "2026-03-01", 0.812), ("rank_v3", "2026-03-08", 0.831),
("rank_v3", "2026-03-15", 0.826), ("rank_v3", "2026-03-22", 0.831),
("recall_v1", "2026-03-01", 0.774), ("recall_v1", "2026-03-08", 0.769),
("recall_v1", "2026-03-15", 0.802),
])
try:
db.execute("""SELECT model, dt FROM runs
WHERE ROW_NUMBER() OVER (PARTITION BY model ORDER BY dt DESC) <= 2""").fetchall()
except Exception as e:
print("❌ 写在 WHERE 里 :", type(e).__name__, "|", e)
try:
db.execute("""SELECT model, dt,
ROW_NUMBER() OVER (PARTITION BY model ORDER BY dt DESC) AS rn
FROM runs WHERE rn <= 2""").fetchall()
except Exception as e:
print("❌ 用别名过滤 :", type(e).__name__, "|", e)
print("\n✅ 先开窗再筛 —— 每个 model 最近 2 次:")
for r in db.execute("""
WITH ranked AS (
SELECT model, dt, auc,
ROW_NUMBER() OVER (PARTITION BY model ORDER BY dt DESC) AS rn
FROM runs
)
SELECT model, dt, auc FROM ranked WHERE rn <= 2 ORDER BY model, dt DESC"""):
print(" ", r)
print("\n⭐ 聚合结果之上再开窗(每个 model 的均值,和它离总体的距离):")
for r in db.execute("""
SELECT model,
ROUND(AVG(auc), 4) AS m,
ROUND(AVG(auc) - AVG(AVG(auc)) OVER (), 4) AS vs_all
FROM runs GROUP BY model"""):
print(" ", r)
实测输出:
对照
❌ 写在 WHERE 里 : OperationalError | misuse of window function ROW_NUMBER()
❌ 用别名过滤 : OperationalError | misuse of aliased window function rn
✅ 先开窗再筛 —— 每个 model 最近 2 次:
('rank_v3', '2026-03-22', 0.831)
('rank_v3', '2026-03-15', 0.826)
('recall_v1', '2026-03-15', 0.802)
('recall_v1', '2026-03-08', 0.769)
⭐ 聚合结果之上再开窗(每个 model 的均值,和它离总体的距离):
('rank_v3', 0.825, 0.0217)
('recall_v1', 0.7817, -0.0217)
⭐ 「组内 Top-N」的标准写法就是这个形状:CTE 里开窗,CTE 外面筛 rn <= N。
06 章那个 WITH 在这里不是为了好看,是唯一的办法。
⚠️ 顺带一个真会咬人的推论:WHERE 里的过滤会改变窗口看到的行集。
「先过滤再排名」和「先排名再过滤」是两个不同的问题,而它们的写法差别只是那句 WHERE 写在 CTE 里面还是外面:
| 想要 | 写法 |
|---|---|
| 只在 3 月的记录里排名次 | ⭐ WHERE 写在 CTE 里面(排名之前就把别的月份剔掉) |
| 在全部记录里排名次,只是最后显示 3 月的 | ⭐ WHERE 写在 CTE 外面(名次是全量算出来的) |
💀 这两条查出来的 rn 完全不同,而两条都不报错。 拿错了,看板上的「第几名」就是错的。
最后那段 AVG(AVG(auc)) OVER () 不是笔误:里层的 AVG 是 GROUP BY 的聚合,外层的 AVG(...) OVER () 是在聚合结果之上再开窗。它能成立,正是因为窗口函数排在 GROUP BY 之后。
🪟 七、你其实已经在用「帧」了
上面所有例子一句帧子句都没写,但它们全都有帧 —— 数据库替你用了默认的那个。
- 排名类(
ROW_NUMBER/RANK/DENSE_RANK)和LAG/LEAD不受帧影响,所以本章一路无事; - ⚠️ 但只要你写下
SUM(x) OVER (ORDER BY dt)这种「聚合 + 窗口内 ORDER BY」的组合, 默认帧就开始起作用了 —— 而它在排序列有并列值时的行为,和几乎所有人的直觉相反。
⭐ 下一章专讲这件事:ROWS 与 RANGE 的差别、BETWEEN … PRECEDING AND CURRENT ROW 怎么写、
以及那个默认帧到底默认成了什么。
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 08-窗口帧.html | ⭐ 下一章,本章刻意欠着的那一半:所有例子都没写帧,但帧一直在起作用。数据有缺口时「最近 3 行」和「最近 3 天」会给出不同的数 |
| 03-聚合与分组.html | 塌行的那一半。⭐ 判据:「每组一行的汇总」用 GROUP BY,「这一行 vs 它那一组」用窗口函数 |
| 01-一条查询是怎么跑的.html | 「窗口函数为什么不能写在 WHERE 里」的完整解释就是那张求值顺序图 —— 它排在 WHERE/GROUP BY/HAVING 之后 |
| 06-CTE与递归查询.html | 组内 Top-N 必须「CTE 里开窗、CTE 外筛选」,这里 CTE 不是为了好看,是唯一的办法 |
| 04-NULL与三值逻辑.html | LAG 在每个分区第一行给 NULL,于是 WHERE delta < 0 会静默丢掉那些行 —— 丢得对,但你得知道它在丢 |
| AI全栈 06b 列表接口与分页 | ⭐ 那一章实证了「排序列大量撞车是常态」(批量导入一秒几百条)。ROW_NUMBER() 取每组第一条时,这正是让结果不稳定的原因 |
| 推荐算法 07 特征工程 | 它要求「统计特征同时喂 ctr_1h / 1d / 7d / 30d」却不给算法 —— 算法就是本章加下一章的帧 |
| 模型上线之后 04 训练推理一致性 | 那里用一段对照伪代码说明「同一个特征两套实现会对不齐」;⭐ 本章补上训练侧那一半到底该怎么写 |
✅ 检查点
- 窗口函数和
GROUP BY最根本的差别是什么?实测那张表 7 行,两种写法各输出几行? diff = auc - AVG(auc) OVER (PARTITION BY model)这一列,为什么GROUP BY写不出来?OVER三个部件各管什么?窗口里的ORDER BY和查询末尾的ORDER BY是一回事吗?- 四行数据里
0.831出现两次,ROW_NUMBER/RANK/DENSE_RANK分别给出哪四个数?三者的最大值各是多少? - 什么时候该用
ROW_NUMBER,什么时候该用DENSE_RANK? ROW_NUMBER()遇到并列时把谁排前面?怎么让它确定下来?这和游标分页里的哪个做法是同一件事?LAG在每个分区的第一行返回什么?实测有几个?WHERE delta < 0会怎么处理这些行、为什么?- 为什么
WHERE ROW_NUMBER() OVER (…) <= 2会报错?报错原文是什么?正确写法是什么形状? - 同一句
WHERE,写在 CTE 里面和外面,算出来的rn为什么不同?哪种情况该写在里面? AVG(AVG(auc)) OVER ()为什么不是笔误?它能成立靠的是什么顺序?
👀 答案
- 塌不塌行。
GROUP BY每组输出一行(7 行 → 2 行),窗口函数输出行数和输入一样(7 行 → 7 行),只是每行多几列关于自己那一组的信息。 - 因为它同时需要「这一行的
auc」和「这一组的均值」。GROUP BY聚合完之后明细行已经不存在了,没有auc可以拿来相减。 OVER (…)声明这是窗口函数(没它SUM就会塌行);PARTITION BY分组,每组各开各的窗(省略则整表一组);窗口内ORDER BY定组内顺序。⚠️ 不是一回事:前者决定「窗里怎么排」,后者决定「结果怎么给你」,两者可以完全不同。ROW_NUMBER→ 1, 2, 3, 4;RANK→ 1, 1, 3, 4(并列后跳号);DENSE_RANK→ 1, 1, 2, 3(不跳号)。最大值分别是 4 / 4 / 3。- 去重、每组取一条用
ROW_NUMBER(它保证每组恰好一个 1);「一共有几档」用DENSE_RANK(它的最大值等于不同取值的个数);「并列第一有两个」这种真实名次用RANK。 - SQL 语义不做承诺,谁在前由数据库决定(本机两次都给了
03-08,但那只是本机这一版的行为)。解法是在排序列末尾补一个唯一列当决胜:加, dt DESC之后 1 号永远是03-22。这和 05 章/AI全栈 06b的复合游标末尾要带唯一列是同一件事。 - 返回
NULL,实测 2 个(两个分区各一个)。WHERE delta < 0会把它们静默丢掉,因为NULL < 0的结果是「不知道」而不是「假」,而WHERE只放行「真」。这次丢掉是对的(第一次重训没有"上一次"),但必须是知道了才让它丢。 - 因为窗口函数排在
FROM → WHERE → GROUP BY → HAVING之后求值 ——WHERE执行时它还不存在。实测报错:misuse of window function ROW_NUMBER();换成用别名过滤是misuse of aliased window function rn。正确形状:CTE 里开窗,CTE 外面WHERE rn <= N。 - 因为
WHERE在窗口函数之前执行,写在 CTE 里面等于先把行剔掉再排名,窗口看到的行集不同。「只在 3 月的记录里排名次」→ 写在里面;「在全量里排名次、只显示 3 月」→ 写在外面。💀 两条都不报错,拿错了看板上的名次就是错的。 - 里层
AVG(auc)是GROUP BY的聚合,外层AVG(…) OVER ()是在聚合结果之上再开窗。它能成立靠的正是窗口函数排在GROUP BY之后这个顺序。实测两组均值 0.825 / 0.7817,离总体各 +0.0217 / −0.0217。
🛑 可以停在这里
⚡ 走神救援
⭐
GROUP BY把十行压成一行;窗口函数让十行还是十行,但每行都多知道一件关于自己那一组的事。⭐ 判据:问题里出现「这一行 vs 它那一组」就是窗口函数,出现「每组一行的汇总」就是
GROUP BY。像「这次的 auc 比本模型平均高多少」这一列,GROUP BY根本写不出来——聚合完明细行就没了。⚠️ 窗口里的
ORDER BY和查询末尾的ORDER BY是两个东西。🥇 排名三兄弟的区别全在并列上:
ROW_NUMBER不并列、RANK并列且跳号(是特性不是 bug)、DENSE_RANK并列不跳号。⭐ 去重和「每组取一条」只能用ROW_NUMBER,因为只有它保证每组恰好一个 1。⚠️ 并列时
ROW_NUMBER把谁排前面,SQL 不做承诺——⭐ 解法是排序列末尾补一个唯一列当决胜,和游标分页的复合游标是同一件事。↔️
LAG/LEAD每个分区各自从头开始,⚠️ 每个分区第一行必然是NULL,于是WHERE delta < 0会静默丢掉它们(NULL < 0是「不知道」不是「假」)。⚠️ 而LAG的第三个参数(取不到时的默认值)把「没有上一次」伪装成「没有变化」,做告警时是危险的。🚦 窗口函数不能写在
WHERE里——因为按执行顺序,WHERE跑的时候它还不存在。所以「组内 Top-N」的标准写法是 CTE 里开窗、CTE 外面筛,这里 CTE 不是为了好看,是唯一的办法。⚠️ 推论会咬人:同一句
WHERE写在 CTE 里面还是外面,算出来的名次完全不同——里面是「先剔掉再排名」,外面是「全量排名只显示一部分」,💀 两条都不报错。
下一节 👉 08-窗口帧.md