假设你手上有一张销售明细表,每一行记录了一笔订单的金额和下单时间。老板想看一个报表:按照时间顺序列出每笔订单,并且每一行都要显示从第一笔订单到当前这笔订单的累计销售总额。这种需求在库存管理、账户流水、积分变动等场景中非常普遍,它本质上是给结果集增加一列“到当前行为止的求和值”。MySQL没有像SUM()那样的简单聚合函数直接返回累积值,但可以通过窗口函数或者用户变量两种方式来实现。

先准备一个测试表,方便后面演示。我们创建一个名为orders的表,包含订单编号、下单时间和金额三个字段,并插入六条模拟数据。后续所有SQL都基于这个表运行。
CREATE TABLE orders (
id INT PRIMARY KEY,
order_time DATETIME,
amount DECIMAL(10,2)
);
INSERT INTO orders (id, order_time, amount) VALUES
(1, '2025-01-01 10:00:00', 100.00),
(2, '2025-01-02 11:30:00', 50.00),
(3, '2025-01-03 09:15:00', 200.00),
(4, '2025-01-03 14:00:00', 75.00),
(5, '2025-01-04 16:45:00', 120.00),
(6, '2025-01-05 08:20:00', 90.00);
接下来分别用两种方法在这张表上计算累积和,先看窗口函数的方案,因为它更直观。
使用窗口函数SUM() OVER计算累积和
MySQL 8.0引入的窗口函数让累积求和变得非常直接。核心语法是SUM(amount) OVER (ORDER BY order_time),它的含义是:按照order_time字段排序,对amount做逐行累加,每一行返回累加到当前行为止的总和。把这条表达式作为一个查询列,就能得到累积和列,不需要任何临时表或者中间变量。
完整的查询语句如下。这里按照订单时间排序,展示订单编号、金额以及累积和列,并且给累积和列起别名cumulative_total。
SELECT
id,
order_time,
amount,
SUM(amount) OVER (ORDER BY order_time) AS cumulative_total
FROM orders
ORDER BY order_time;
执行后结果非常干净。第一行金额100.00,累积和也是100.00;第二行金额50.00,累积和变成150.00;第三行金额200.00,累积和跳到350.00,依此类推。窗口函数内部会自动维护一个“窗口帧”,默认帧范围是从分区起始行到当前行,所以每前进一行,就把当前行的amount加进累加器。你不需要显式声明ROWS BETWEEN,但如果你希望控制帧的范围,比如只累加当前行和前一行,可以写成SUM(amount) OVER (ORDER BY order_time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW),那就是移动求和而不是累积求和了。
窗口函数还支持分区累积。假如订单表里有一个customer_id字段,你想分别计算每个客户的订单累积金额,只需要在OVER子句中加入PARTITION BY。例如SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_time),这样每个客户内部的累加会重新从零开始,互不影响。这是用户变量方案很难优雅实现的功能——用户变量要实现分区累积需要额外判断分组键变化并手动重置变量,代码会变得很啰嗦。
使用窗口函数时有一个容易忽略的点:ORDER BY的排序列如果存在重复值,累积和的结果是不确定的。假设两条订单的order_time完全相同,窗口函数在计算时无法保证内部稳定排序,可能会因为执行计划的不同导致两条记录的前后顺序变化,进而产生不同的累积和。解决办法是在ORDER BY中追加一个唯一键,比如写成ORDER BY order_time, id,确保排序完全确定。这一点对于金额类场景尤其重要,因为重复时间戳在批量导入的数据中并不少见。
使用用户变量实现累积和(MySQL 5.7及以下)
很多线上系统仍然运行着MySQL 5.6或5.7,这些版本不支持窗口函数,此时用户变量就成了经典解法。基本思路是:先对数据排好序,然后在查询中利用@cumulative这样的变量保存上一行的累计值,当前行的累积和等于变量值加上当前行的amount。表达式通常写成@cumulative := @cumulative + amount,同时需要把变量初始化为0。
最直接的写法是在同一个SELECT里完成初始化、排序和赋值。比如下面这条语句,看起来应该能工作:
SET @cumulative := 0;
SELECT
id,
order_time,
amount,
(@cumulative := @cumulative + amount) AS cumulative_total
FROM orders
ORDER BY order_time;
但这段SQL存在一个严重的问题:MySQL官方文档明确说明,在同一个查询语句中同时读取和写入同一个用户变量时,赋值顺序是未定义的。换句话说,SELECT列表中@cumulative := @cumulative + amount的求值时机可能与ORDER BY的执行次序不一致。在MySQL 5.7的实际测试中,有时排序之后会先计算所有行的表达式再排序输出,导致累积和完全错误。即使在某些版本上碰巧得到正确结果,也不应该依赖这种未定义行为。
为了避免这个问题,需要把排序和变量赋值拆成两个阶段。先将数据排序写入一个派生表(子查询),然后在外层查询中对派生表做变量累加。外层查询不需要ORDER BY,MySQL会按照派生表产生的行顺序依次处理,这样变量赋值的顺序就与排序后的行序一致了。正确写法如下:
SET @cumulative := 0;
SELECT
t.id,
t.order_time,
t.amount,
(@cumulative := @cumulative + t.amount) AS cumulative_total
FROM (
SELECT id, order_time, amount
FROM orders
ORDER BY order_time, id
) AS t;
这里在子查询中使用了ORDER BY order_time, id,用id作为排序的稳定键,保证结果确定。外层查询没有ORDER BY,派生表的顺序会被保留下来(从MySQL 5.7开始优化器通常会保留派生表顺序,但严谨起见,如果外层仍然需要保证输出顺序,可以在最外层再套一层查询并排序)。经过这样处理后,变量累加就严格按照时间顺序执行,结果和窗口函数版完全一致。
用户变量方案还有两个细节需要留意。第一,如果amount列存在NULL值,@cumulative + amount会导致NULL传播,累加结果直接变成NULL,后续所有行都跟着变成NULL。在计算累积和之前需要用IFNULL(amount, 0)或COALESCE(amount, 0)处理。第二,变量需要每次查询前重置为0,可以在同一个会话中先执行SET @cumulative := 0;,或者使用内联初始化技巧,例如SELECT ... FROM (SELECT @cumulative := 0) AS init, ...,但后者可读性较差,不建议在生产代码中使用。
两种方法的对比与选择建议
窗口函数方案在可读性和正确性上全面胜出。它不需要维护外部状态,语法表达的就是“按照某个顺序做累加”,执行计划由优化器统一管理,不容易因为语句结构变化而产生隐藏bug。而用户变量方案本质上是在查询过程中手动维护一个累加器,依赖于行处理顺序的稳定性,一旦外层多了一层排序、或者优化器改变了执行计划,结果就可能悄悄出错。从维护成本看,窗口函数代码更容易让接手的人看懂并安全修改。
性能方面,窗口函数在MySQL 8.0中经过专门优化,对于排序加累加这类操作,通常采用一次排序加一次扫描的方式完成,效率很高。用户变量方案虽然也只需要一次排序,但派生表会额外消耗临时表,当数据量很大时,临时表带来的磁盘I/O可能让查询变慢。实际测试中,百万行级别的数据,窗口函数往往比用户变量方案快15%到30%,当然具体差距取决于表结构和索引情况。如果你的表在order_time列上有索引,窗口函数可以利用索引顺序避免显式排序,性能优势会更加明显。
兼容性是用户变量方案唯一的强项。如果你的数据库还停留在MySQL 5.7甚至更早,无法升级,那只能用它。但即便在这种情况下,也要严格遵循“子查询先排序、外层再累加”的模式,并加上唯一键保证排序稳定,同时处理NULL值。对于新项目,只要数据库版本在8.0以上,应该毫不犹豫使用窗口函数。另外,如果业务中还需要分区累积(按客户、按地区分组),窗口函数的PARTITION BY写起来非常简单,而用户变量实现分区累积需要判断分组键变化并重置变量,代码复杂且容易写错。
最后总结一下:累积和列在MySQL中没有原生函数,但有两条路可以走。窗口函数方案适用于MySQL 8.0及以上,代码简洁、结果可靠、性能更好;用户变量方案适用于旧版本,但必须小心赋值顺序和NULL处理。做技术选型时,先看数据库版本,再看数据量,最后考虑可维护性。如果你的环境允许,优先升级到8.0并使用窗口函数,这会让后续报表开发轻松很多。