Menu

SQLite 窗口函数:OVER、PARTITION BY 与窗口框架

SQLite 中的窗口函数怎么用:OVER、PARTITION BY、排名函数、LAG/LEAD,以及用于计算累计值的窗口框架子句。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

窗口函数增加一列,但不合并行

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,就能让累计总和在每个分组内重新开始计算。

Coddy 编程语言插图

和 Coddy 一起学编程

开始