怎样用 SQL 日期函数统计自然月环比

咖啡半糖
咖啡半糖
发布时间:2026-08-25| 183人浏览过
原创

前言

做月度报表时,很多 SQL 看起来能算出“上月对比”,结果却经常因为月份对齐方式不对、数据库版本受限或时间口径不一致而失真。本文把自然月环比拆成定义、MySQL 8.0+ 写法、低版本兼容方案和边界检查四部分,帮你判断查询是否算对,也能看懂为什么同一份数据会在不同写法下出现偏差。

自然月与滚动时间窗口的差异示意图,突出按日历月聚合再比较的正确口径
自然月对齐为什么比“减一个月”更重要先把日期归到自然月,再谈环比,能避免跨年和大小月错位。

做月度报表时,很多人会先想到“当前日期减 1 个月”,但自然月环比并不是滚动 30 天比较,而是把数据严格落在每个日历月里再做前后对照。下面按定义、MySQL 8.0+ 推荐方案、低版本兼容写法和常见坑点拆开说明,方便你判断该用哪种 SQL,以及结果是否真的可信。

自然月环比到底在算什么

自然月环比的公式是:

(本月数据 - 上月数据)/ 上月数据 × 100%

这里最关键的不是公式,而是“自然月”三个字。它要求按日历月对齐,也就是每个月都以 1 号到月末为完整区间,而不是固定 30 天,也不是从“今天往前推一个月”的动态窗口。

这也是为什么不能直接把 DATE_SUB(NOW(), INTERVAL 1 MONTH) 当成自然月比较条件。动态偏移只适合做滚动分析,一旦碰到跨年、大小月,统计口径就会错位。例如 1 月和 12 月的对比,真正要比的是 1 月 1 日到 1 月 31 日,对上 12 月 1 日到 12 月 31 日,而不是围绕某一个具体日期往前推。

更稳妥的方式,是先把原始日期归到“年月”这一层,再按月聚合。常见写法有两类:

YEAR(date_col)
MONTH(date_col)

或者直接拼出年月标识:

CONCAT(YEAR(date_col), '-', LPAD(MONTH(date_col), 2, '0'))
DATE_FORMAT(date_col, '%Y-%m')

有了统一的 year_month 之后,再去做分组、聚合和上月值关联,逻辑会清楚很多,也更不容易在跨年时出错。

MySQL 8.0+:用窗口函数计算月度环比

如果数据库是 MySQL 8.0+,优先用 LAG()。它的优势很直接:先得到每个月的聚合结果,再按年月排序,把上一行当成上月值取出来,写法清晰,后期维护也方便。

MySQL 8.0+ 用 LAG() 计算月度环比的步骤型示意图
LAG() 方案的正确顺序MySQL 8.0+ 适合先月度聚合,再按 year_month 排序用 LAG() 取上月值。

第一步,先按自然月聚合。比如统计每月订单总额:

SELECT DATE_FORMAT(order_time, '%Y-%m') AS year_month, SUM(amount) AS monthly_amount FROM orders WHERE order_time >= '2024-01-01' GROUP BY DATE_FORMAT(order_time, '%Y-%m')

第二步,在聚合结果外层使用 LAG() 拉取上月数据,并计算环比:

SELECT year_month, monthly_amount, LAG(monthly_amount) OVER (ORDER BY year_month) AS last_month_amount, ROUND((monthly_amount - LAG(monthly_amount) OVER (ORDER BY year_month)) / LAG(monthly_amount) OVER (ORDER BY year_month) * 100, 2) AS mom_rate FROM (/* 上面的聚合子查询 */ ) t

这套写法能成立,有两个前提必须满足。

先聚合,再做 LAG()

很多报表算错,不是 LAG() 有问题,而是直接在明细表上套窗口函数,没有先汇总到月粒度。这样一来,同一个月内的多条记录都会参与窗口计算,结果自然不是“月对月”。所以正确顺序一定是:先按 DATE_FORMAT(order_time, '%Y-%m') 聚合,再在月度结果集上取上月值。

必须显式排序

LAG() 是基于结果顺序取前一行,没写 ORDER BY year_month,上月值就可能乱序。原文里提到的常见错误,本质上都是这个问题:排序缺失,或者排序字段不是统一格式的年月字段。

影响自然月环比准确性的边界问题卡片图,包括时间口径、时区和空值处理
结果失真通常出在口径与数据质量报表失真多数不是公式错,而是时间口径、时区或 NULL 处理没有统一。

另外,首月没有上月可比数据,LAG() 返回 NULL。这种情况下,环比结果也会是 NULL。如果报表不需要展示首月,可以显式过滤:

WHERE last_month_amount IS NOT NULL

这样做的好处是语义明确,前端也不容易误把“无可比项”当成正常业务数值。

MySQL 5.7 及以下:用自连接补上月数据

如果系统还停留在 MySQL 5.7 或更早版本,没有窗口函数,就只能走“先聚合、再自连接”的路线。思路并不复杂:当前月结果表和自身做一次关联,把能对应到上月的那一行连出来。

问题主要出在“怎么准确构造上月年月”。直接硬算年和月会非常绕,例如下面这种思路虽然可行,但可读性很差:

STR_TO_DATE(CONCAT(YEAR(date_col)-IF(MONTH(date_col)=1,1,0), '-', LPAD(MONTH(date_col)-1+IF(MONTH(date_col)=1,12,0), 2, '0'), '-01'), '%Y-%m-%d')

实际更推荐先把 year_month 还原成当月 1 号,再减去 1 个月,最后格式化回 %Y-%m。对应写法是:

DATE_FORMAT(DATE_SUB(STR_TO_DATE(CONCAT(year_month, '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH), '%Y-%m')

这样处理的好处是跨年场景会自动对齐,比如从 2024-01 推回 2023-12,不需要手工拆分 1 月和其他月份分别判断。

在实现上,可以按下面这个结构理解:

  • 先得到聚合结果表,记为 t_curr,包含 year_month 和 amount
  • 再把它左连接到自身的上一月结果表 t_prev
  • 关联条件使用年月减一月后的结果进行匹配

原始关联逻辑可写为:

t_curr.year_month = DATE_FORMAT(DATE_SUB(STR_TO_DATE(CONCAT(t_prev.year_month, '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH), '%Y-%m')

如果首月没有上月数据,还要避免它参与后续环比运算。原文建议补上:

AND t_prev.year_month IS NOT NULL

这类方案常见于老系统无法升级,或者同一套报表要兼容多版本 MySQL 的场景。缺点也很明显:大数据量下,自连接通常比窗口函数更慢。因此如果报表长期使用,最好在 year_month 字段上建立索引,减少连接和分组的开销。

写对 SQL 之后,还要盯住这些边界问题

自然月环比最容易出问题的地方,往往不是 SQL 语法,而是时间字段的业务口径不一致。看上去都是某个时间列,实际含义可能完全不同:订单创建时间、支付时间、发货时间、结算时间,各自代表的业务阶段并不一样。你按哪个字段分组,最后得到的“月度变化”就对应哪个业务动作。

所以在动手写 SQL 前,至少要确认三件事:

时间字段到底代表什么

例如订单表里的 order_time 是创建时间,但财务月报也许按支付时间或发货时间结算。如果这个口径没先对齐,后面的环比公式再正确,也只是把错误口径算得很工整。

时区和入库延迟有没有统一

一些数据会在凌晨批量写入,或者由跨时区系统采集。如果没有统一时区,比如没有先归一到 UTC+8,那么月末最后几个小时的数据可能会落进下个月,导致 DATE_FORMAT(order_time, '%Y-%m') 的分组结果失真。

空值、非法日期和除零要单独处理

像 '0000-00-00' 这样的非法日期,可能让 DATE_FORMAT 返回 NULL,最终整组数据都进不到正常月份里。再比如 ROUND(..., 2) 在除零或 NULL 的情况下会返回 NULL,前端展示时不能直接把它当成 0,否则“没有上月数据”和“本月增长 0%”就会被混成一类。

换句话说,报表里看到 NULL 时,不一定是 SQL 写错了,也可能是在提醒你:这条记录没有可比的上月基准,或者源数据本身就不满足计算条件。把这类情况在接口层或前端层单独标注出来,通常比强行补 0 更符合真实业务含义。

该选哪种方案,取决于数据库版本和报表要求

如果环境是 MySQL 8.0+,优先使用 LAG(),可读性和维护性都更好,也更适合后续继续扩展同比、累计值等分析指标。前提是先完成月度聚合,并保证 ORDER BY year_month 明确存在。

如果还需要兼容 MySQL 5.7 及以下版本,就采用“年月聚合 + 自连接”的方案,把 year_month 转成当月 1 号后再 DATE_SUB 一个自然月,避免手工处理跨年月份。无论哪种写法,最终结果是否可信,都要回到三个检查点:时间字段业务口径是否统一、时区是否一致、空值与 NULL 是否被正确解释。

只要这几个点没跑偏,自然月环比的 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 算法库。