如何使用 SQL 窗口函数计算累计总额?

周予安
周予安
发布时间:2026-08-24| 50人浏览过
原创

前言

累计总额是报表和明细统计里很常见的一类需求,但一旦把它误写成 `GROUP BY` 聚合,或者忽略排序稳定性,查出来的数字就可能看似正确、实际不可靠。本文从窗口函数 `SUM() OVER()` 的基础写法讲起,再补上排序键选择与大表性能这两个容易被忽略的判断点,帮助你快速判断一条累计 SQL 到底能不能放心上线。

累计总额看起来只是“把前面的值不断加起来”,真正落到 SQL 查询时,难点其实在两个地方:一是结果必须保留每一行,二是累加顺序必须稳定可复现。只要把这两点想清楚,就能判断什么时候该用窗口函数,什么时候即便语法能跑,结果也未必可信。

下面按“能不能用 GROUP BY、基础写法怎么写、排序为什么会出错、数据量大时怎么避免变慢”四个问题来拆开说明,方便你直接套到日常报表和明细统计里。

什么是累计总额,为什么不能用 GROUP BY

累计总额(running total),就是按照某个字段定义的顺序,逐行把当前值累加起来,得到“截至当前这一行”为止的总和。典型场景是按日期累计销售额、按流水号累计收入,或者按事件时间累计用户操作次数。

展示窗口函数累计总额与 GROUP BY 聚合输出差异的白底信息图
累计总额与 GROUP BY 的输出差异累计总额要求保留明细行,和分组聚合的输出形态并不相同。

这里最容易混淆的是:累计总额不是普通聚合。它要求保留原始行数,并且让每一行都有一个对应的累计结果。

GROUP BY 做不到这一点,因为它的目标是把多行压缩成分组结果。例如你按日期分组,它返回的是“每个日期的汇总值”;而累计总额要的是“每一条记录到当前为止的累计值”。两者看起来都用了 SUM(),但输出形态完全不同。

因此,如果你需要的是逐行增长的动态和,就不能依赖 SUM() + GROUP BY,而要换成窗口函数来处理。

用 SUM() OVER() 实现基础累计和

最常见的写法是:

SUM(column) OVER (ORDER BY key)

这类写法的核心在于:SUM() 负责求和,OVER (...) 定义窗口规则,窗口函数本身不会压缩行数,所以每一行都能拿到自己的累计结果。

几种常见形式如下:

  • SUM(sales) OVER (ORDER BY date):按 date 升序,从第一行累计到当前行
  • SUM(sales) OVER (PARTITION BY region ORDER BY date):先按 region 分区,再在每个分区内部按 date 累加
  • 默认帧范围通常可理解为 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是“从开头到当前行”

一个基础示例如下,适用于 PostgreSQL、MySQL 8.0+、SQL Server:

SELECT date, sales,
 SUM(sales) OVER (ORDER BY date) AS running_total
FROM sales_data;

如果你的业务不是全表累计,而是“每个区域各算各的”,只需要把分区字段加进去:

SELECT region, date, sales,
 SUM(sales) OVER (PARTITION BY region ORDER BY date) AS running_total
FROM sales_data;

理解这一层后,可以把窗口函数当成一种“在不打散明细记录的前提下做统计”的方式。它和聚合函数的最大区别,不在于函数名,而在于是否保留明细行。

ORDER BY 缺失或重复时,累计结果为什么不可靠

很多累计总额查询“语法没报错,结果却时好时坏”,问题通常不在 SUM(),而在排序规则。

展示 ORDER BY 稳定性对累计总额结果影响的白底信息图
排序稳定性如何影响累计结果累计总额是否可信,取决于排序规则是否明确且能唯一确定行序。

首先,如果没有显式写 ORDER BY,SUM() OVER() 的累加顺序就不可靠。数据库可能按物理存储顺序、读取顺序或执行计划中的临时顺序处理这些行,而这些顺序并不是固定不变的。换一次执行计划、做一次 VACUUM、调整数据写入顺序,结果都可能变化。

其次,即便写了 ORDER BY,如果排序键本身不唯一,累计结果仍然可能不稳定。最常见的例子是同一天有多笔订单,或者多条记录拥有相同的 created_at 时间戳。此时数据库只能保证“相同排序值的记录大致排在一起”,却不一定保证它们彼此之间的先后次序。

一旦相对顺序变化,累计值就会在这些重复键之间重新分配,表现出来就是你看到的数字“忽高忽低”或者“同一批数据重跑结果不同”。

怎么让累计顺序稳定下来

  • 始终显式写出 ORDER BY
  • 让排序字段能够唯一确定行序,例如使用 ORDER BY date, id
  • 不要只依赖 ORDER BY created_at 这类可能重复的字段
  • 如果业务上长期需要稳定顺序,可以在源表增加专用排序字段,例如自增 seq

一个更稳妥的例子是:

展示大表执行窗口累计和时排序成本与优化方向的白底信息图
大表累计查询的性能关注点窗口累计和的主要成本通常来自排序,优化重点在索引和缩小参与计算的数据范围。
SELECT date, id, sales,
 SUM(sales) OVER (ORDER BY date, id) AS running_total
FROM sales_data;

这类写法的意义不只是“多写一个字段”,而是明确告诉数据库:当 date 相同的时候,还要按 id 决定先后。只有顺序可重复,累计值才可重复。

大表上的性能问题:排序才是真正的成本

累计总额的语法通常很短,但执行代价未必低。原因在于 OVER (ORDER BY ...) 本质上需要先对窗口数据进行排序,而排序往往是大表查询最贵的一步。

当数据量上到 100 万行以上,如果排序字段没有合适索引,查询很可能明显变慢;在内存不足时,还可能把排序结果写入磁盘临时文件。这个问题不是窗口函数“不能用”,而是执行计划没有为排序成本兜底。

优化时优先看这几点

  • 确保 ORDER BY 字段有索引
  • 如果同时用了 PARTITION BY 和 ORDER BY,优先考虑能覆盖这两部分的复合索引
  • 不要在全表上先算累计、再做过滤;更合理的方式通常是先 WHERE 过滤,再在子查询或 CTE 里执行 OVER
  • 在 MySQL 8.0 中,ORDER BY 后配合 ROWS UNBOUNDED PRECEDING 通常不会额外带来明显性能问题,但使用 RANGE 帧时可能更慢,因为它需要做值范围比较

也就是说,累计总额的性能优化方向很明确:先确认排序是否必要、排序字段是否稳定,再检查是否有索引支撑,最后看是否能缩小参与窗口计算的数据范围。

对很多报表 SQL 来说,慢不慢并不取决于 SUM(),而是取决于你让数据库为多少行做排序,以及排序后的数据能不能直接从索引里拿到。

写累计总额时,最该先检查的 3 件事

如果你只想快速判断一条 SQL 是否写对,可以先检查下面三件事:

  1. 是不是用了窗口函数而不是 GROUP BY,并且结果保留了原始行数;
  2. 排序条件是否显式、稳定、可唯一确定先后;
  3. 大表场景下,排序字段和过滤顺序是否考虑过执行计划成本。

累计总额本身并不复杂,真正容易出问题的是这些看起来像“细节”的地方。顺序没定义清楚,数字就可能今天对、明天错;排序没有索引,查询就可能从秒级变成分钟级。

只要把“保留明细行、稳定定义顺序、控制排序代价”这三点一起把住,SUM() OVER() 就是 SQL 里处理累计总额最直接、也最可靠的方案之一。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。