窗口函数增加一列,但不合并行
GROUP BY 会把多行归并成一行。窗口函数的做法不同:它在一组相关的行上计算一个值,但输出中保留每一个输入行。逐行的明细和汇总结果,你可以并排同时看到。
它的形式总是一样的:一个函数,后面跟着 OVER (...)。
total_all 列显示的是所有行的总和,在每一行上重复出现,原始的行原封不动。对比一下 SELECT SUM(amount) FROM sales:数字一样,但只返回一行。窗口函数让你同时得到这两种视图。
PARTITION BY:在分组内汇总
空的 OVER () 会在整张表上汇总。加上 PARTITION BY 就能在分组内汇总,很像 GROUP BY,但同样不会合并行。
每一行都得到了所在地区的总额,以及自己在其中所占的比例。如果用普通的 GROUP BY,你就会丢掉每位员工的明细。这正是窗口函数最大的优势:一个查询里同时拿到明细和汇总。
排名:ROW_NUMBER、RANK、DENSE_RANK
排名函数按照 OVER 里的 ORDER BY 给行编号。三种函数的区别在于如何处理并列。
解读输出:
ROW_NUMBER()总是唯一的,并列时会随意分出先后。需要给每一行一个稳定且互不相同的编号时用它。RANK()给并列的行相同的排名,然后跳过后面的数字。两位玩家并列第 1,下一位就是第 3。DENSE_RANK()同样允许并列,但不跳号,下一个排名是 2。
要实现“每组前 N 名”,可以把排名函数和 PARTITION BY 结合起来,再在外层查询里筛选,因为 WHERE 不能直接引用窗口函数:
每个地区销售额最高的两个人。
LAG 和 LEAD:查看相邻的行
LAG(col) 返回窗口中上一行的 col 值,LEAD(col) 则往后取。两者都非常适合回答“随时间变化了多少”这类问题。
第一行的 yesterday 是 NULL,因为它前面没有行。你可以提供一个默认值:LAG(celsius, 1, celsius) OVER (ORDER BY day) 会在没有上一行时使用当天的值。
LEAD 正好相反。把两者和 PARTITION BY 结合,就能得到按分组的序列,比如在每个地区内把本月销售额和上个月比较。
用窗口框架计算累计值
在 OVER 里加上 ORDER BY 后,SUM、AVG、COUNT 这类聚合函数就开始累计计算:
有两点需要注意:
SUM(amount) OVER (ORDER BY day)是累计总和。只写ORDER BY而不明确指定框架时,默认框架是“从窗口开头到当前行”。- 第二列使用了显式框架:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW。这是一个 3 行的滑动窗口,也就是移动平均。
可以这样理解框架:每个窗口函数都在一个行框架上计算,这个框架是相对于当前行定义的。常见的框架有:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:累计总和(隐式默认值)。ROWS BETWEEN N PRECEDING AND CURRENT ROW:向前追溯的窗口。ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING:整个分区。
ROWS 按物理行计数。另外还有 RANGE,它按值分组:当 ORDER BY 列里有并列值、而你希望把它们当作同一步处理时,它就很方便。
FIRST_VALUE、LAST_VALUE、NTILE
还有几个值得了解的窗口函数:
FIRST_VALUE和LAST_VALUE返回框架内的第一个或最后一个值。使用LAST_VALUE时要留意框架:默认框架止于CURRENT ROW,所以通常要写ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING才能拿到分区里真正的最后一个值。NTILE(n)把行分成n个大致相等的桶,适合计算四分位数、百分位数,或者做 A/B 测试式的分组。
用 WINDOW 给窗口命名
当好几列共用同一个 OVER (...) 子句时,反复书写会很繁琐。SQLite 允许你给窗口命名一次,然后重复使用:
同样的查询,干扰更少。WINDOW 子句写在 WHERE/GROUP BY/HAVING 之后、ORDER BY 之前。
窗口函数与 GROUP BY 的区别
两者都涉及聚合,但回答的是不同的问题:
GROUP BY做归并,每组一行。只想要汇总结果时用它。- 窗口函数做保留,每个输入行都会留下,旁边附上额外计算出的列。
如果你发现自己先做了 GROUP BY,然后又把聚合结果连接回原表,这就是一个强烈的信号:窗口函数可以在一个查询里完成这件事。
几个常见陷阱
WHERE不能引用窗口函数。 筛选发生在窗口计算之前。把查询包进子查询或 CTE 里,在外层做筛选。- 隐式框架容易坑人。
SUM(x) OVER (ORDER BY y)之所以是累计总和,是因为默认框架是RANGE UNBOUNDED PRECEDING。如果你想要整个分区的总和,就写不带ORDER BY的OVER (PARTITION BY ...),或者显式指定框架。 LAST_VALUE第一次用时人人都会意外。 默认框架止于当前行,所以它返回的是当前行的值,而不是分区的最后一个值。记得覆盖框架。- 窗口函数需要 SQLite 3.25 及以上版本(2018 年发布)。稍新一点的安装都支持,但有些嵌入式环境的版本会落后。
下一篇:生成列
窗口函数是在查询时计算。下一页讲的是在存储时计算:生成列,也就是列的值由一个表达式定义,并在底层数据变化时自动更新。
常见问题
SQLite 中的窗口函数是什么?
窗口函数会在与当前行相关的一组行上计算一个值,但不会像 GROUP BY 那样把它们合并成一行。你在 ROW_NUMBER()、RANK()、SUM() 或 LAG() 这类函数后面加上 OVER (...) 子句来定义窗口。每一个输入行都会保留在结果里,你只是多得到一个计算出来的列。
SQLite 中的 RANK 和 DENSE_RANK 有什么区别?
两者都根据 ORDER BY 分配排名,但处理并列的方式不同。RANK() 在并列之后会留出空缺:两行并列第 1,下一行就是第 3。DENSE_RANK() 不留空缺,下一行是第 2。想要连续的排名就用 DENSE_RANK(),空缺本身有意义时用 RANK()。
如何在 SQLite 中计算累计总和?
使用带窗口框架的 SUM(column) OVER (ORDER BY ...)。默认情况下,OVER 里的 ORDER BY 使用 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 这个框架,得到的就是累计总和。再加上 PARTITION BY,就能让累计总和在每个分组内重新开始计算。