SQL中如何用SUM OVER窗口函数计算累计求和?

来源:JS脚本作者:落伍者头衔:草根站长
导读:本期聚焦于落伍者创作的《SQL中如何用SUM OVER窗口函数计算累计求和?》,敬请观看详情。订单报表里经常会遇到这样一种需求:按日期显示每日销售额,同时要在每一行后面带上从月初到当天的累计销售额。普通GROUP BY只能得到每日小计,要算累计值,过去往往靠自连接、相关子查询或MySQL变量,这些写法不仅复杂,数据量大时性能也堪忧。SQL标准中的窗口函数SUM OVER可以一次性完成累计求和,核心是ORDER BY指定累计顺序,PARTITION BY按分组累计。本文通过销售、账户流水等场景,演示SUM OVER的基础语法,讲解分区键、排序键的选择,比较ROWS与RANGE两种窗口范围,并指出重复排序值、NULL值、性能优化等容易踩到的细节。掌握这个技巧后,累计求和、滚动汇总、分组占比等需求都能用一条SQL清晰实现。

要按日期展示每日销售额,同时带上从第一天到当天的累计销售额,这种需求在报表和财务对账中很常见。常规做法是先按日期分组算出每日小计,再用自连接或相关子查询把截止当天的数据加总。这样做代码啰嗦,数据量一大,性能往往非常差。其实SQL标准中的窗口函数早就提供了更简洁的方案,也就是SUM函数配合OVER子句,一条查询就能完成累计求和。它的基本思路是:在每一行上,根据ORDER BY指定的顺序,计算从窗口起点到当前行的SUM值,不会像GROUP BY那样把多行折叠成一行。

SQL中如何用SUM OVER窗口函数计算累计求和?

一、基础写法:ORDER BY决定累计方向

最基础的累计求和使用SUM(amount) OVER (ORDER BY order_date)。这里的窗口没有PARTITION BY,意味着整个结果集是一个窗口。ORDER BY order_date告诉数据库按日期升序排列,当前行以及之前所有行的amount都会被加总。查询返回的仍然是一行一条明细,只是多了一列running_total,每行代表截至该日期的累计销售额。

SELECT
    order_date,
    amount,
    SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders
ORDER BY order_date;

上面这段SQL中,窗口函数的计算逻辑可以这样理解:先对数据按order_date排序,然后从第一行开始,依次取当前行之前(含当前行)的所有amount求和。排序字段不一定是日期,也可以是自增主键、交易流水号等,只要它能表达业务上的先后顺序。比如按id升序累计,就写成SUM(amount) OVER (ORDER BY id)。如果想按日期倒序,从最近一天往前累计,可以写ORDER BY order_date DESC。总之,累计方向完全由ORDER BY控制,这是很多初学者容易忽略的地方。

还要注意,窗口函数在SELECT阶段计算,不会改变结果集行数。与GROUP BY不同,它不会把相同日期的多条记录合并。如果同一天有多笔订单,每一行都会返回,累计值会根据窗口范围规则逐行或按相同键处理。这个细节在第三节会展开。

二、按用户或分类分区累计:PARTITION BY的使用

很多场景下累计量并不是全局累加,而是分组独立累加。比如统计每个用户的消费累计、每个商品的销量累计、各个地区的订单金额累计。这时候需要在OVER子句中增加PARTITION BY。它的作用是把数据划分成多个独立分区,窗口函数在每个分区内单独计算,分区之间互不影响。看下面这个例子:订单表中有user_id,要计算每个用户自己的累计消费金额。

SELECT
    user_id,
    order_date,
    amount,
    SUM(amount) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS user_running_total
FROM orders
ORDER BY user_id, order_date;

这个查询按照user_id把订单拆成多个窗口,每个用户一个窗口,再按order_date排序累计。当user_id从1001切换到1002时,累计值会重新从0开始。这样得到的user_running_total就是每个用户自己的历史累计消费,而不是所有用户混在一起。PARTITION BY后面可以跟多个字段,比如PARTITION BY region, user_id,表示先按地区再按用户分片。分区的本质是在内存或排序过程中对数据进行分组计算,不需要额外嵌套子查询。

需要注意,PARTITION BY和ORDER BY没有先后强制关系,但语法上PARTITION BY必须写在ORDER BY之前。如果窗口函数只有PARTITION BY而没有ORDER BY,那么它计算的是整个分区的总和,每一行都会返回相同的分区合计值。比如SUM(amount) OVER (PARTITION BY user_id)就是每个用户的总消费金额,常见于算占比或排名时配合使用。理解了这一点,就能灵活组合出不同的统计口径。

三、ROWS与RANGE:重复排序值如何影响累计结果

在累计求和里,有一个容易被忽视的细节:当ORDER BY的字段存在重复值时,默认窗口范围RANGE和物理行范围ROWS会产生不同结果。先看默认行为。SQL标准规定,窗口函数如果只写ORDER BY而不写窗口范围,默认等价于RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。RANGE模式按排序键的值来划分窗口,相同排序键的所有行会被视为一组。比如订单表里同一天有3笔订单,金额分别是100、200、300。按日期累计时,这3行的order_date相同,RANGE模式下它们的累计值都会是600,也就是当天所有订单加总后的值,而不是第一行100、第二行300、第三行600。

要想严格按物理行的先后顺序逐行累加,需要显式使用ROWS窗口。例如:

SELECT
    order_date,
    amount,
    SUM(amount) OVER (
        ORDER BY order_date, id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS rows_total
FROM orders
ORDER BY order_date, id;

这里加了第二个排序字段id,用来在相同日期内确定行的先后顺序。窗口范围改成ROWS后,每一行只累加从窗口起点到当前行所包含的物理行,不会因为order_date相同而把后面的行提前合并。结果就是同一天的3笔订单依次得到100、300、600。如果业务需要逐笔流水累加,而不是按日期聚合后的累计,必须使用ROWS。尤其在交易流水、日志分析、账户余额变动等场景,ROWS更符合直觉。

除了累计求和,窗口范围还可以做滚动汇总。比如ROWS BETWEEN 6 PRECEDING AND CURRENT ROW表示当前行加上往前6行,共7行求和,适合计算近7天销量。RANGE则更适合按日期区间聚合,例如RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW,但MySQL 8对日期区间的RANGE支持有限,实际使用ROWS偏多。理解这两个窗口的差异后,就能根据排序键是否有重复值、业务是否需要逐行累计来选择合适的写法。

四、替代写法与性能对比:为什么推荐窗口函数

在窗口函数普及之前,很多开发人员用相关子查询或自连接来算累计求和。典型的写法是把同一张表取两个别名,外层表驱动每一行,内层子查询汇总所有小于等于当前日期的记录。查询语句大致如下:

SELECT
    o1.order_date,
    o1.amount,
    (
        SELECT SUM(o2.amount)
        FROM orders o2
        WHERE o2.order_date <= o1.order_date
    ) AS running_total
FROM orders o1
ORDER BY o1.order_date;

这种写法在数据量较小时可行,但它的复杂度通常是O(n²)。如果orders表有几万行,子查询就要扫描几万次几万行,耗时可能急剧上升。换成SUM OVER窗口函数后,数据库通常只需要对数据做一次排序,再线性扫描一遍即可完成累计,复杂度接近O(n log n)。在SQL Server、PostgreSQL、Oracle、MySQL 8以上的执行计划中,窗口函数往往表现为一个Window Aggregate算子,配合Sort算子,数据扫描次数明显减少。

另外,早期MySQL 5.x没有窗口函数,开发者会用用户变量模拟累计,例如先按日期排序,然后用@running变量逐行累加。这种方案依赖结果集顺序和变量赋值时机,一旦查询优化器调整了执行计划,或者SQL中有ORDER BY、GROUP BY干扰,结果很容易出错。窗口函数是标准语法,逻辑声明式、可读性高,数据库优化器还有机会做更多优化。因此,只要数据库版本支持,累计求和应该优先选择SUM OVER。

性能优化方面,如果表很大,排序字段和分区字段最好建立合适的索引。比如订单表经常按user_id分区、按order_date排序,可以建(user_id, order_date)联合索引。但要注意,窗口函数仍然可能触发额外排序,当查询结果本身就按相同顺序返回时,优化器可能省去一次Sort。对于并发较高的OLTP系统,窗口函数通常用于报表和离线分析,不建议在高频交易核心链路上直接扫描大表做全量累计。实际项目中可以通过定时任务把累计值固化到汇总表,或者借助物化视图、列存引擎进一步加速。

五、实战变形:滚动N日求和与累计占比

掌握了SUM OVER的基础用法,可以延伸出很多实用的统计口径。最常见的是滚动N日求和。比如要计算每笔订单日期往前7天的销售额总和,可以写:

SELECT
    order_date,
    amount,
    SUM(amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS last_7_days_total
FROM orders
ORDER BY order_date;

这里ROWS BETWEEN 6 PRECEDING AND CURRENT ROW表示窗口包含当前行以及它前面的6行,一共7行。如果每天只有一条汇总记录,结果就是近7天销售额。如果同一天有多条明细,仍然需要结合id排序,否则每天的滚动累计会把当天后面的行也计算进来,含义会偏向最近7笔而不是最近7天。这种细节在开发时要结合数据粒度判断清楚。

另一个常见的变形是累计占比。比如想观察销售额随时间的累积贡献,可以同时计算全局总量和累计量,再把两者相除。窗口函数允许一个SELECT中同时出现多个聚合窗口。例如:

SELECT
    order_date,
    SUM(amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total,
    SUM(amount) OVER () AS grand_total,
    SUM(amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) * 100.0 / SUM(amount) OVER () AS running_percent
FROM orders
ORDER BY order_date;

这里SUM(amount) OVER ()没有ORDER BY和PARTITION BY,表示从整个结果集求总量。每个窗口独立计算,互不干扰。running_percent就是累计百分比。该写法省去了子查询和GROUP BY,代码清晰很多。需要注意的是,如果amount存在NULL,SUM会忽略NULL值,如果某行amount为NULL又希望计入0,可以用COALESCE处理。另外,当分母为0或NULL时,除法的结果可能为NULL,需要根据业务用CASE或NULLIF做保护。

总的来说,SUM OVER窗口函数把累计求和从复杂的过程式写法中解放出来,让SQL更接近业务描述:按什么顺序、在哪个范围内、对哪一列做累加。实际使用时,牢记ORDER BY控制方向、PARTITION BY控制分组、ROWS和RANGE控制窗口边界,就能应对绝大多数累计统计需求。配合合适的索引和数据结构设计,窗口函数在报表、对账、指标分析等场景中都是高效可靠的选择。

累计求和SUM OVER窗口函数修改时间:2026-10-06 20:36:13

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1006/66577.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。