直接用 SUM() OVER() 算余额易出错,因未设期初余额且 created_at 排序不唯一;须显式添加期初行、ORDER BY created_at, id,并按业务类型条件聚合,同时建立 account_id, created_at, id 联合索引。

为什么直接用 SUM() OVER() 算余额容易出错
很多人一上来就写 SUM(amount) OVER (ORDER BY created_at),结果发现余额对不上——不是漏了初始余额,就是没处理同一时间多笔交易的排序不确定性。窗口函数本身不关心业务语义,它只按指定顺序累加,而银行账户余额必须满足两个硬约束:① 起点是期初余额(不是 0),② 同一毫秒的多笔操作必须有确定顺序(否则重放结果不一致)。
常见错误现象:SUM() OVER() 返回值比手工核对少一笔、余额突然跳变、导出数据每次运行结果不同。
- 必须显式加入期初余额行(比如用
UNION ALL插入一条amount = 10000的记录,并设created_at = '2024-01-01 00:00:00') - 排序字段不能只有
created_at,要补上唯一列(如id或transaction_id)避免并列时非确定性:ORDER BY created_at, id - 如果表里有撤销/冲正交易(
amount为负),确保它们已归入同一张明细表,而不是靠应用层过滤后计算
SUM() OVER() 的 ORDER BY 必须包含唯一键
MySQL 8.0+、PostgreSQL、SQL Server 都支持窗口函数,但只要 ORDER BY 子句中存在重复值,数据库就可能任意打乱相同 created_at 的行顺序。这意味着你昨天跑出的余额序列,今天再跑可能中间几行数字就变了——尤其在批量导入或日志回放场景下极危险。
正确做法是把业务主键或自增 ID 加进排序:
SELECT id, created_at, amount, 10000 + SUM(amount) OVER (ORDER BY created_at, id) AS balance FROM account_transactions WHERE status = 'success';
注意:这里 10000 是期初余额,硬编码仅适用于简单场景;生产环境建议从另一张 account_summary 表查出 opening_balance 并 JOIN 进来。
如何处理「先记账后确认」类交易(如冻结、解冻)
真实账户系统常有“可用余额”和“实际余额”之分,比如转账时先冻结资金(产生 type = 'freeze' 记录),到账后再确认(type = 'settle')。这时不能把所有 amount 无差别累加。
你需要按业务类型做条件聚合:
- 用
CASE WHEN过滤只计入影响实际余额的操作:SUM(CASE WHEN type IN ('deposit', 'withdrawal', 'settle') THEN amount ELSE 0 END) - 若需同时看可用余额,可另起一列:
SUM(CASE WHEN type IN ('deposit', 'withdrawal', 'unfreeze') THEN amount ELSE 0 END) - 避免在
WHERE中过滤掉冻结类记录——否则窗口排序会丢失位置,导致后续 settle 行的累计值错位
性能隐患:大表上 SUM() OVER(ORDER BY ...) 会全表扫描
当 account_transactions 超过百万行,且没有合适索引时,ORDER BY created_at, id 可能触发 filesort,执行计划显示 Using temporary; Using filesort。这不是语法问题,而是优化器找不到覆盖索引。
必须建联合索引:
CREATE INDEX idx_acc_trans_order ON account_transactions (account_id, created_at, id);
注意三点:
-
account_id放最左,因为查询一定带账户维度过滤(否则算的是全平台余额) - 不要只建
(created_at, id)——缺少account_id会导致索引无法用于WHERE account_id = ? - 如果经常按日期范围查(如最近 7 天),确保
created_at在索引中位置靠前,让 range scan 生效
窗口函数本身无法下推谓词,所以 WHERE 条件越早过滤掉无关账户和状态,性能越好。

















