在我处理过的财务系统项目里,窗口函数这块确实是块硬骨头。尤其是用SQL做财务报表,看起来简单,但细节一不留神就踩坑。今天专门聊聊窗口函数在财务实战中的几个关键技巧,从累计发生额到环比同比,再到余额表取数,最后说说索引优化,一次性讲透。

财务报表里怎么用SUM() OVER()做累计发生额
财务最常碰到的就是“本月累计”和“本年累计”这类计算。直接用GROUP BY会丢掉明细行,用子查询嵌套又太深,代码可读性直线下降。正确做法其实很简单,就用SUM() OVER(),但排序和范围必须明确。
核心思路是:必须指定ORDER BY,而且字段得能体现时间先后,比如account_date,否则结果不可靠。默认窗口是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这个默认设置对日期字段容易出问题——同一天有多笔分录时,如果不加id做区分,系统就会把当天所有行一股脑全算进来,累计就错了。
- 稳妥写法:
SUM(amount) OVER (PARTITION BY account_code ORDER BY account_date, id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)——加个id字段,就是防止日期重复导致累计错乱。 - 如果要“按月累计”,别只写
ORDER BY YEAR(account_date), MONTH(account_date),得先生成唯一序号再排序,否则同月多笔还是会乱序。 - MySQL 8.0+ 和 PostgreSQL 支持ROWS帧,SQL Server也支持;但Oracle默认用RANGE,日期重复时行为不同,上线前务必验证一下。
环比/同比怎么写才不出错:LAG()的空值和偏移陷阱
财务分析里,“比上月涨了多少”“比去年同期增减”是标配,LAG()是主力工具。但两个坑踩中一个就全崩:空值没处理,或者偏移量设错。
比如LAG(amount, 1) OVER (ORDER BY account_date)取上一行,但首行一定是NULL。如果后面直接除或减,整列就会变成NULL,整个报表就算废了。更隐蔽的是,按自然月算环比,不能简单偏移1行——2月只有28天,3月有31天,你偏移第29行,根本不是“上月同日”。
- 强制补零:
COALESCE(LAG(amount, 1) OVER (ORDER BY account_date), 0),避免后续计算报错。 - 按日历对齐同比:
LAG(amount, 365) OVER (ORDER BY account_date)仅适用于平年,闰年得用DATE_SUB(account_date, INTERVAL 1 YEAR)关联,而不是偏移行数。 - 月份级环比,建议先聚合到月粒度,再拉偏移。直接用
LAG(amount) OVER (PARTITION BY YEAR(account_date), MONTH(account_date) ORDER BY account_date)不成立,逻辑上就是错的。
科目余额表怎么取“最新一条余额”而不漏数据
余额表不是静态快照,每笔凭证更新后都会生成新行。要取每个account_code下account_date最大的那条,很多人习惯写ROW_NUMBER() OVER (PARTITION BY account_code ORDER BY account_date DESC),然后WHERE rn = 1。结果发现某些科目消失了——因为account_date相同、id不同,ORDER BY不稳定,rn分配随机,导致漏数据。
- 必须加确定性排序:
ROW_NUMBER() OVER (PARTITION BY account_code ORDER BY account_date DESC, id DESC)。 - 别用RANK()或DENSE_RANK()——它们对并列值给相同排名,会导致多行rn = 1,余额重复,结果就乱了。
- 如果表里有update_time字段,优先用它代替id,更贴近业务含义。
- 注意:MySQL 5.7 不支持窗口函数,执行会直接报错
FUNCTION ROW_NUMBER does not exist,先查SELECT VERSION()确认版本。
为什么ORDER BY字段没索引,财务月报跑10分钟?
窗口函数本身不建临时表,但OVER()里的ORDER BY会触发全量排序。一张500万行的凭证表,如果account_date没有索引,SUM() OVER (ORDER BY account_date)就会拖慢整个查询。PostgreSQL的执行计划里会出现Sort节点,MySQL 8.0+ 的EXPLAIN FORMAT=TREE会显示window_function + filesort,一看就知道问题出在哪。
- 复合索引要严格匹配:
(account_code, account_date)才能加速PARTITION BY account_code ORDER BY account_date。 - 单列索引
account_date对纯时间排序有效,但加了PARTITION BY后效果打折。 - 财务系统常有“按期间查询”,索引字段顺序必须是分区字段在前、排序字段在后,反了就用不上。
真正容易被忽略的不是语法,而是窗口函数的执行阶段——它在GROUP BY之后、HA VING之前运行,但又不能引用未出现在SELECT或GROUP BY里的列。写完先看执行计划,再查版本兼容性,最后验数据稳定性。这三个步骤,一个都不能少。
