“过去30天每天的环比增长率”指逐日滚动计算(当日值−前一日值)/前一日值×100%,需确保日期连续(缺日补0)、分母不为0、首日结果为NULL;必须用日历表补全日期+LAG()取前日值+NULLIF防除零。

什么是“过去30天每天的环比增长率”
环比增长率 =(当日值 − 前一日值)/ 前一日值 × 100%,前提是前一日有数据。它不是和固定基准比,而是逐日滚动比较。很多人误以为只要用 LAG() 就完事,但实际要解决两个关键问题:日期必须连续(哪怕某天无数据也要补0)、分母不能为0、起始日没有“前一日”要跳过或标记为 NULL。
用 LAG() + 日历表生成连续日期
原始业务表(比如 sales)通常只存有交易记录的日期,缺勤日不存行。直接 LAG() 会跳过空日,导致“昨日”错位。必须先构造一个包含过去30天完整日期的临时日历表,再左连接业务数据。
实操建议:
- 用递归 CTE 或
generate_series()(PostgreSQL)/DATEADD()循环(SQL Server)生成日期序列,避免硬写30行 - 左连接时用
COALESCE(sales.amount, 0)补0,否则LAG()会把空值当真实值参与计算 -
LAG(amount, 1) OVER (ORDER BY date)的排序字段必须是日历表的date,不是原始表的日期字段
示例(PostgreSQL):
WITH date_series AS (
SELECT CURRENT_DATE - INTERVAL '29 days' + INTERVAL '1 day' * n AS dt
FROM generate_series(0, 29) AS n
),
daily_sum AS (
SELECT ds.dt::date AS date, COALESCE(SUM(s.amount), 0) AS amount
FROM date_series ds
LEFT JOIN sales s ON ds.dt::date = s.sale_date
GROUP BY ds.dt::date
)
SELECT
date,
amount,
ROUND(
(amount - LAG(amount, 1) OVER (ORDER BY date))::DECIMAL
/ NULLIF(LAG(amount, 1) OVER (ORDER BY date), 0) * 100, 2
) AS growth_rate
FROM daily_sum;处理分母为0和首日异常
首日没有“前一日”,LAG() 返回 NULL,除法会得 NULL;若某日金额为0,下一日计算时分母为0会报错 division by zero。不能靠外围 WHERE amount > 0 过滤,那会破坏日期连续性。
关键做法:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 用
NULLIF(denominator, 0)替代直接除,避免报错,返回NULL - 用
ROUND(..., 2)控制小数位,防止浮点误差干扰判断 - 如果业务允许,可将首日
growth_rate显式设为0或'N/A',但需在应用层或CASE中处理,数据库里保持NULL更严谨
MySQL 和 SQL Server 的兼容写法差异
MySQL 8.0+ 支持 LAG(),但不支持 generate_series();SQL Server 用 master..spt_values 或 CTE + UNION ALL 模拟日历表更常见。核心逻辑不变,但日期生成方式要换。
MySQL 示例片段(生成日期):
WITH RECURSIVE dates(dt) AS ( SELECT DATE_SUB(CURDATE(), INTERVAL 29 DAY) UNION ALL SELECT DATE_ADD(dt, INTERVAL 1 DAY) FROM dates WHERE dt < CURDATE() )
SQL Server 示例片段(避免递归深度限制):
SELECT DATEADD(day, number, DATEADD(day, -29, GETDATE())) AS dt FROM master..spt_values WHERE type = 'P' AND number BETWEEN 0 AND 29
所有版本都要注意:窗口函数的 ORDER BY 必须明确指定,且不能带 WHERE 过滤后再开窗,否则日期顺序可能被破坏。
真正难的不是写公式,是让“昨天”严格等于日历上的前一天——哪怕那天没生意。漏掉补0逻辑,或把业务表日期当排序依据,结果就全偏了。

















