同比是与去年同期比较(如2024-03 vs 2023-03),环比是与上一期比较(如2024-03 vs 2024-02);直接用LAG()不可靠,因其依赖行序而非时间逻辑,遇缺失期、跨年或非月粒度时易出错;正确做法是先按时间维度聚合,再通过JOIN或补全时间序列后的窗口函数计算,确保“同期”定义准确。

什么是同比和环比,为什么不能直接用 LAG() 算增长率
同比是和去年同期比(比如 2024-03 vs 2023-03),环比是和上一期比(比如 2024-03 vs 2024-02)。直接用 LAG() 只能拿到相邻行,对月度数据可能凑巧对得上,但遇到缺失月份、跨年、或按周/季度聚合时就会错——LAG(value, 1) 拿到的不一定是“上个月”,更不是“去年同月”。
真正靠谱的做法是:先按时间维度聚合出每期数值,再用 JOIN 或窗口函数关联对应同期数据。核心在于明确“同期怎么定义”,而不是依赖行序。
用 LEFT JOIN 算同比:按年月分组后自连接
适合大多数业务场景,逻辑清晰、兼容性好(所有 SQL 方言都支持),且能自然处理某月无数据的情况(结果为 NULL)。
- 先用
GROUP BY YEAR(date), MONTH(date)或DATE_FORMAT(date, '%Y-%m')聚合销售额等指标 - 把聚合结果表别名为
t_curr,再LEFT JOIN同一表t_prev,条件是t_prev.year = t_curr.year - 1 AND t_prev.month = t_curr.month - 增长率公式为:
(t_curr.amount - COALESCE(t_prev.amount, 0)) / NULLIF(t_prev.amount, 0)—— 注意用NULLIF防除零,用COALESCE把去年空值转 0 或保持NULL视业务而定
SELECT t_curr.yr_mon, t_curr.amount AS curr_amount, t_prev.amount AS last_year_amount, ROUND((t_curr.amount - COALESCE(t_prev.amount, 0)) / NULLIF(t_prev.amount, 0), 4) AS yoy_rate FROM ( SELECT DATE_FORMAT(order_date, '%Y-%m') AS yr_mon, SUM(amount) AS amount FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ) t_curr LEFT JOIN ( SELECT DATE_FORMAT(order_date, '%Y-%m') AS yr_mon, SUM(amount) AS amount FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ) t_prev ON t_prev.yr_mon = DATE_FORMAT(DATE_SUB(STR_TO_DATE(CONCAT(t_curr.yr_mon, '-01'), '%Y-%m-%d'), INTERVAL 1 YEAR), '%Y-%m');
用窗口函数 LAG() + 时间偏移算环比,但必须先补全时间序列
如果原始数据本身缺某些月份(比如 2024-02 没订单),LAG() 会跳过空档,把 2024-03 的“上期”连到 2024-01,导致错误。所以必须先生成完整的时间序列,再左连业务数据。
- 用递归 CTE 或日历表生成连续的年月(如从最小日期到最大日期每月一行)
-
LEFT JOIN业务聚合结果,确保每期都有行(空则amount = NULL) - 在此基础上用
LAG(amount) OVER (ORDER BY yr_mon)才安全 - 注意:PostgreSQL/MySQL 8.0+/SQL Server 支持标准窗口语法;SQLite 不支持
LAG,需改用子查询
关键点不是函数本身,而是输入数据是否“连续”。没补全时间轴就套 LAG,环比数字看着漂亮,实际是错的。
不同数据库对日期函数的写法差异很实际
写一次 SQL 跑遍 MySQL、PostgreSQL、SQL Server 几乎不可能。最常踩坑的是年月提取和日期偏移:
- MySQL:用
DATE_FORMAT(date, '%Y-%m')和DATE_SUB(date, INTERVAL 1 YEAR) - PostgreSQL:用
TO_CHAR(date, 'YYYY-MM')和date - INTERVAL '1 year' - SQL Server:用
FORMAT(date, 'yyyy-MM')(2012+)或CONVERT(VARCHAR(7), date, 120),偏移用DATEADD(YEAR, -1, date) - BigQuery:用
FORMAT_DATE('%Y-%m', date)和DATE_SUB(date, INTERVAL 1 YEAR)
别指望一个写法通用。上线前务必在目标环境里用真实数据跑一遍,尤其验证跨年 1 月 → 去年 1 月的逻辑是否正确。很多线上问题就出在 2023-01 关联不到 2022-01,因为字符串拼接或截取漏了前导零。

















