结论:留存率与流失率需用两套独立$group统计;必须预存格式统一的register_date字段,$dateFromString须指定"%Y-%m-%d"格式,时间差计算前需转Date并$trunc截断时分秒。

直接说结论:留存率和流失率必须用两套独立的 $group 阶段分别统计,不能靠单次分组加条件判断;关键不是“怎么写”,而是“字段怎么存、日期怎么对齐、索引建在哪”。
必须预存 register_date 字段并统一格式
行为日志(如 retention.login)里不能只存 date,必须显式带上用户注册日期 register_date,且值要和注册表(retention.register)中完全一致——字符串格式推荐 "2026-05-01",别用 ISODate 或毫秒数。
-
$dateFromString必须指定format: "%Y-%m-%d",否则无法解析非 ISO 字符串,会返回null - 如果用
$subtract手动算天数,两个时间都得先转成Date类型,再用$trunc截掉时分秒,否则 5 月 1 日 14:00 注册、5 月 2 日 09:00 登录会被算成 0 天(差值 - 不统一格式会导致
$dateDiff报错或返回null,后续$group会直接丢弃该文档
$dateDiff 计算天数时 unit 要选 "day"
MongoDB 5.0+ 推荐用 $dateDiff,它按自然日计算,自动处理时区、月末、闰年等边界,比手动除 86400000 更准更稳。
-
unit: "day"表示完整日历日差,5 月 1 日注册、5 月 2 日任意时刻登录 →days_since_register = 1 - 别用
"hour"或"second"再自己除,容易因浮点误差导致第 1 天/第 7 天统计偏移 - startDate 和 endDate 必须都是 Date 类型,
$dateFromString的输出需确认是否成功转换(可加$cond判断是否为null)
嵌套 $group 实现“同注册日 → 按留存天数分桶”
单个 $group 只能产出一个维度的聚合结果,而留存率需要两个维度:注册日(分母) + 行为距注册天数(分子)。必须拆成两步:
- 第一层
$group:按register_date分组,用$sum: 1统计每日新增用户数(即分母) - 第二层
$group:按register_date和days_since_register联合分组,统计每个注册日下第 N 天回访人数(即分子) - 最后用
$lookup或应用层 join 合并两组结果,算比值;MongoDB 不支持管道内跨组除法,硬要在 DB 层算得用$merge写回临时集合
索引不建在 date 上,而要建在 register_date 和 date
聚合里最慢的环节往往不是计算,是扫描。如果你只在 date 字段建了索引,但 $match 过滤的是 register_date,索引就完全失效。
-
retention.register集合:对date建升序索引(注册日查询高频) -
retention.login集合:必须对register_date和date同时建复合索引,顺序为{ register_date: 1, date: 1 } - 漏掉
register_date索引,$match阶段一上来就要全表扫,千万级日志下聚合可能卡住几分钟
最容易被忽略的是:流失率不是“没登录”的补集,而是“注册后连续 N 天无行为且未再次活跃”的明确状态;它需要额外标记用户最后一次行为时间,并用 $max 和日期比较判断“是否已超期”,这比留存率多至少一个 $group + $project 阶段。

















