结论:统计连续达标月份必须用ROW_NUMBER()生成序号后计算“日期-序号”差值构造分组键,而非DATE_SUB硬减;因月份是字符串或整数,无原子加减语义,跨年跳月或数据缺失时硬减会出错,且业务连续指日历相邻而非字典序。
直接说结论:统计连续达标月份,不能用
硬减,必须用
生成序号后做“日期 - 序号”差值构造分组键。否则遇到跨年、跳月、数据缺失时,结果全错。
为什么月份不能像日期一样直接相减
月份是字符串(如
)或年月整数(如
),没有内置的“加1个月”原子语义。用
或
强转会出问题:
返回的是 3 月 1 日的天数,但你根本不知道该月是否有记录;
得到
,可你的数据里月份字段是
,类型不一致导致 JOIN 或比较失败;
更关键的是:业务定义的“连续月份”指日历上相邻的月份(如 2025-03 → 2025-04),不是字符串字典序连续(
→
可以,但
→
字典序就断了)。
正确做法:把月份标准化为日期再算差值
核心是统一转换成可计算的日期起点(比如每月 1 日),再套用经典“序号差法”:
先确保月份字段是
或
类型;如果不是,用
转成
;
用
安全转成当月首日;
对每个用户,按月份升序排,生成
;
计算分组键:
;
这个
在连续月份中恒定——例如
、
、
对应的
都是
。
达标条件必须在分组前过滤,不能后筛
连续达标 ≠ 连续月份中“有达标记录”,而是“所有月份都达标”。常见错误是先求出所有连续月份段,再
——这会漏掉中间某月没数据但业务上算“未达标”的情况。
正确顺序是:
第一步:只取
的记录(即“达标月”);
第二步:对这些达标月执行上述
+ 差值分组;
第三步:按
和
分组,
就是该段连续达标月份数。
注意:如果某用户在 2025-03 达标、2025-04 缺失、2025-05 达标,这两段会被拆成两个独立
,不会被误判为连续 —— 这正是你要的逻辑。
MySQL 8.0+ 示例片段(可直接跑)
最后一句容易被忽略:
和
必须从原始
字段取,别用
格式化输出——因为
是日期类型,显示为
,而你要的是
。
DATE_SUB(month, INTERVAL n MONTH)ROW_NUMBER()'2025-03'202503DATE_ADD()TO_DAYS()TO_DAYS('2025-03')DATE_ADD('2025-03-01', INTERVAL 1 MONTH)'2025-04-01''2025-04''2025-03''2025-04''2025-12''2026-01'CHAR(7)DATESTR_TO_DATE(month_str, '%Y-%m')DATESTR_TO_DATE(CONCAT(month, '-01'), '%Y-%m-%d')rn = ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY month)grp = DATE_SUB(STR_TO_DATE(CONCAT(month, '-01'), '%Y-%m-%d'), INTERVAL rn - 1 MONTH)grp2025-032025-042025-05grp'2025-03-01'HAVING COUNT(*) = COUNT(CASE WHEN score >= 80 THEN 1 END)score >= 80ROW_NUMBERuser_idgrpCOUNT(*)grpWITH qualified AS (
SELECT user_id, month,
STR_TO_DATE(CONCAT(month, '-01'), '%Y-%m-%d') AS dt
FROM user_monthly_score
WHERE score >= 80
),
ranked AS (
SELECT user_id, month, dt,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY month) AS rn
FROM qualified
),
grouped AS (
SELECT user_id, month,
DATE_SUB(dt, INTERVAL rn - 1 MONTH) AS grp
FROM ranked
)
SELECT user_id, MIN(month) AS start_month, MAX(month) AS end_month, COUNT(*) AS streak_months
FROM grouped
GROUP BY user_id, grp
ORDER BY user_id, start_month;MIN(month)MAX(month)monthgrpgrp'2025-03-01''2025-03'