group by login_date 无法计算准确留存率,因其仅统计每日活跃用户数而非首次登录用户数;正确做法是先按 user_id(及分组维度如 channel)计算每人首次登录日,再按该日期分组并用 left join + on 条件匹配回访。

GROUP BY 本身不能直接算出留存率,它只能统计“当天有多少人登录”,而留存率的分母必须是“当天首次登录的人”——这个前提不满足,后续所有计算都失真。
为什么 GROUP BY login_date 会把留存率算错
常见错误写法:SELECT login_date, COUNT(DISTINCT user_id) FROM user_logins GROUP BY login_date。这得到的是每日 DAU(活跃用户数),不是某日新增用户数。比如用户 A 在 6 月 1 日、6 月 3 日、6 月 5 日都登录过,这条语句会在三天里各计一次,但他在留存分析中只应属于「6 月 1 日 cohort」的分母,且只算一次。
- 分母必须是「首次登录发生在当天」的用户集合,不是「当天有登录」的用户集合
-
GROUP BY login_date没法区分一个用户是第几次登录,更无法锁定其首次行为日 - 若表中存在
login_date IS NULL,MIN()可能返回NULL,导致整个分母污染
必须先算 first_login,再按它分组
核心是把「每个用户的首次登录日」提前算出来,再按这个日期分组。兼容性好,MySQL / Hive / Spark SQL 都支持:
- 用
GROUP BY user_id算每人MIN(login_date),得到first_login - 加
WHERE login_date IS NOT NULL,避免空值干扰MIN()结果 - 若需按渠道分组(如
channel),GROUP BY必须包含channel, user_id,否则跨渠道混入分母 - 限定 cohort 范围:加
WHERE first_login BETWEEN '2026-09-01' AND '2026-09-30'
LEFT JOIN 回访记录时,日期条件必须放 ON 里
例如查次日留存,要匹配 login_date = DATE_ADD(first_date, INTERVAL 1 DAY)。这个条件如果写在 WHERE 子句里,会导致没回访的用户被过滤掉,LEFT JOIN 退化成 INNER JOIN:
- 错误写法:
LEFT JOIN daily_login d ON f.user_id = d.user_id WHERE d.login_date = ...——WHERE会过滤掉d为NULL的行 - 正确写法:
LEFT JOIN daily_login d ON f.user_id = d.user_id AND d.login_date = DATE_ADD(f.first_date, INTERVAL 1 DAY) - 判断是否留存,靠
d.user_id IS NOT NULL,而不是count(*)直接除
分组维度(如 channel)必须参与 first_login 计算
如果想看不同渠道的次日留存,first_login 不能是全量用户的首次登录日,而必须是“该渠道内该用户的首次登录日”。否则会出现:某用户在渠道 A 首次登录是 6 月 1 日,在渠道 B 首次登录是 6 月 5 日,但全局 MIN() 把他算进 6 月 1 日 cohort,却放在渠道 B 的分母里。
- 用子查询时:
GROUP BY channel, user_id是底线,漏掉channel就等于没分组 - 用窗口函数时:
MIN(login_date) OVER (PARTITION BY channel, user_id) - JOIN 时也要对齐维度:
AND d.channel = f.channel,否则关联错位
真正容易被忽略的点在于:first_login 不是一个固定值,它随分组维度动态变化;而日期偏移匹配一旦漏写在 ON 条件里,零留存用户就彻底消失,分母变小、结果虚高——这不是精度问题,是逻辑错误。










