登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 窗口函数累计值跨月份时怎么重置分组

来源:17golang原创

时间:2026-09-09 10:33:26 380浏览 收藏

报表里常见一种“累计值没有归零”的错觉:一月最后一笔是 1200,二月第一笔却从 1250 开始。问题通常不在 SUM(),而在窗口分区仍按账户或整张结果集划分,没有把“月份”作为业务周期放进 PARTITION BY。在 MySQL 8.0 及以上,先生成包含年份的月份键,再按账户和月份分区,累计值就会在每个新月份从第一条记录重新开始。

最小修复是:PARTITION BY account_id, month_key,其中 month_key 必须包含年份;同时用 event_time, event_id 做稳定排序,并显式写出 ROWS UNBOUNDED PRECEDING

要点速览
  • 月份分组不要只取月份数字,跨年报表必须使用 YYYY-MM 这样的周期键。
  • PARTITION BY 决定累计何时重置,ORDER BY 决定月内每一行看到的先后范围。
  • 同一时间戳有多条数据时,加上唯一的 event_id,避免累计结果依赖不稳定的行顺序。

先把月份变成窗口分区键

先假设有一张收入明细表 account_income,字段包括账户、发生时间、明细主键和金额。不要直接写 MONTH(event_time):2025 年 1 月和 2026 年 1 月都会得到 1,两个周期会被错误地放进同一分区。

WITH marked AS (
    SELECT
        event_id,
        account_id,
        event_time,
        amount,
        -- 年月共同组成周期键,避免跨年串组
        DATE_FORMAT(event_time, '%Y-%m') AS month_key
    FROM account_income
)
SELECT
    account_id,
    month_key,
    event_time,
    amount,
    SUM(amount) OVER (
        PARTITION BY account_id, month_key
        ORDER BY event_time, event_id
        ROWS UNBOUNDED PRECEDING
    ) AS monthly_running_total
FROM marked
ORDER BY account_id, month_key, event_time, event_id;

这里的职责很明确:account_id 防止不同账户互相累计,month_key 让每个自然月成为独立窗口,amount 是真正参与求和的数值。窗口函数不会像 GROUP BY 那样把明细压成一行,而是给每一条明细保留一个累计结果。

MySQL窗口函数按账户和包含年份的月份键划分累计窗口的结构关系图
图1:把 account_id 与包含年份的 month_key 组成窗口分区,跨月累计才会在新月份重新开始。

累计窗口要明确排序和帧范围

PARTITION BY 只解决“在哪些行之间累计”,还需要 ORDER BY 解决“累计到哪一行”。如果 event_time 不是唯一值,单独按时间排序就不够稳定,因此用明细主键 event_id 做第二排序键。ROWS UNBOUNDED PRECEDING 表示从当前月份分区的第一行累计到当前行。

SQL 部分回答的问题常见误区
PARTITION BY account_id, month_key什么时候重置累计只按账户,导致跨月延续
ORDER BY event_time, event_id月内按什么顺序走时间相同却没有唯一补充键
ROWS UNBOUNDED PRECEDING当前行看到多大范围依赖默认帧,遇到同值排序键难解释

如果业务要按财务月、自然周或门店营业日重置,做法完全一样:先在 CTE 或子查询里生成业务周期键,再把这个键纳入分区。不要为了“归零”在外层套一层条件表达式,那样容易掩盖窗口边界没有定义的问题。

MySQL SUM OVER 使用稳定排序和ROWS帧计算月内累计值的查询结构图
图2:稳定排序和显式 ROWS 帧共同限定每一行看到的月内累计范围。

用边界行快速核对结果

改完 SQL 后,先挑一个账户同时包含月末和次月月初的数据核对:月末累计应等于该月明细总和,次月第一条累计应等于次月第一笔金额。再检查跨年同月,例如 2025-01 与 2026-01 是否各自从零开始。若出现重复或跳跃,优先检查时间列时区、重复事件、金额是否为 NULL,以及排序键是否真的唯一。

  • 按月汇总核对:SUM(amount) GROUP BY account_id, month_key 应与每月最后一行累计值一致。
  • 检查分区键:不要用 MONTH(event_time) 单独分组;周期键至少包含年。
  • 检查排序稳定性:发生时间相同时,使用自增主键或其他唯一业务序号。
  • 检查业务定义:自然月、财务月和滚动 30 天不是同一个窗口,不能只改显示格式。

常见问题

只按 month_key 分区可以吗?

如果结果只统计全体账户,可以;只要要按账户分别累计,就必须把 account_id 也放进 PARTITION BY,否则不同账户会共享同一个月度累计。

为什么不直接用 GROUP BY 月份?

GROUP BY 适合得到每月一行的总额;窗口函数适合保留每笔明细并显示截至当前行的累计值。两者不是谁替代谁,而是输出粒度不同。

时间相同的两条记录一定会算错吗?

不一定会报错,但如果排序键不唯一,哪条记录先出现可能不稳定。加上 event_id 后,结果更容易复现、审计和测试。

把“重置周期”写进 PARTITION BY,把“月内次序”写进 ORDER BY,再用显式 ROWS 固定累计范围,这个思路也适用于周、季度和自定义结算周期。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>