应选LAG()标流失时间点、MIN()标首次登录;因LAG可计算登录间隔以判定断连,MIN则高效获取首登日且不易受NULL干扰。

用 LAG() 和 MIN() 窗口函数识别用户首次登录与流失时间点
留存率和流失周期的核心是判断用户“来过几次”以及“最后一次来之后是否再没来”。不能只靠 COUNT() 聚合,必须保留用户行为的时间序列。关键不是算总数,而是对每个用户按时间排序后打标记。
常见错误是直接对用户分组后取 MIN(event_time) 和 MAX(event_time),这会丢失中间断连信息——比如用户 1月、3月、5月登录,2月和4月没来,但 MAX - MIN 给出的是 4个月“活跃跨度”,而非“是否在2月流失”。
- 对每个用户用
ORDER BY event_time+ROW_NUMBER()标记登录序号,再用LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)拿到上一次登录时间,就能计算两次登录间隔 - 流失判定依赖“间隔是否超过阈值”(如7天),所以必须先算出每条记录的
prev_login,再用event_time - prev_login > INTERVAL '7 days'判定是否断连 -
MIN(event_time) OVER (PARTITION BY user_id)比子查询更安全,避免因 NULL 或多行聚合导致首次时间错位
用 DATE_TRUNC() 对齐周期并计算次日/7日留存率
留存率本质是分母为某日新增用户、分子为该用户在后续某日仍出现的比例。难点不在除法,而在如何把“新增”和“回访”落到同一周期维度上——必须统一按登录日期的周/月/日截断,否则时间对不齐。
PostgreSQL/BigQuery 支持 DATE_TRUNC('day', event_time),MySQL 需用 DATE(event_time);若用周留存,注意不同数据库周起始日不同(PostgreSQL 默认周日,可用 DATE_TRUNC('week', event_time + INTERVAL '1 day') - INTERVAL '1 day' 强制周一为起点)。
- 先用
MIN(event_time) OVER (PARTITION BY user_id)算出每个用户的首次登录时间,再用DATE_TRUNC('day', first_login)得到“新增日期” - 对每个用户,用
LEAD(DATE_TRUNC('day', event_time), 1) OVER (PARTITION BY user_id ORDER BY event_time)可快速拿到次日是否回访(非绝对次日,而是下一次登录日) - 真正做留存率时,需两层嵌套:外层按
cohort_date分组,内层用COUNT(CASE WHEN return_day = cohort_date + 1 THEN 1 END)统计次日回访数
用布尔聚合和 FILTER(或条件 CASE)避免中间表爆炸
计算 30 日留存时,如果先生成每个用户每天是否登录的宽表(user_id × 30列),数据量会指数级膨胀。正确做法是用聚合函数直接统计逻辑结果,不展开稀疏行为。
PostgreSQL 支持 BOOL_AND() 和 BOOL_OR(),配合 FILTER (WHERE ...) 子句可精准提取某类行为;其他数据库用 COUNT(CASE WHEN ... THEN 1 END) 替代,但要注意分母必须是去重用户数,不能是总记录数。
- 次日留存率 =
COUNT(DISTINCT CASE WHEN return_day = cohort_date + 1 THEN user_id END) * 1.0 / COUNT(DISTINCT user_id) - 流失周期中位数不能用
MEDIAN()(多数数据库不原生支持),改用PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY gap_days)更可靠 - 若用户在首日之后从未再登录,
LAG()返回 NULL,对应 gap_days 为 NULL,需在统计前用WHERE gap_days IS NOT NULL过滤,否则中位数被污染
MySQL 用户注意窗口函数版本限制与替代方案
MySQL 8.0+ 才支持标准窗口函数,5.7 及以下必须用自连接或变量模拟 LAG(),极易出错。例如用 @prev := IF(@uid = user_id, @prev, NULL) 依赖执行顺序,而 MySQL 不保证 ORDER BY 在变量赋值前生效,结果不可靠。
更稳妥的做法是升级到 8.0+,或改用临时表:先用 GROUP BY user_id 汇总每个用户的最小/最大登录时间,再关联原表做间隔计算。虽然慢,但语义清晰、结果确定。
- MySQL 8.0 中
LAG()的OFFSET参数默认为 1,不可省略;写成LAG(event_time) OVER (...)是合法的,但显式写LAG(event_time, 1)更易读 - 用
STR_TO_DATE()解析字符串时间时,若格式不统一(如 '2023-01-01' vs '2023/01/01'),LAG()会返回 NULL,务必先清洗或用COALESCE()补缺 - 大表上跑多层窗口函数容易 OOM,建议先用
WHERE event_time >= '2023-01-01'限定范围,再加user_id IN (SELECT user_id FROM active_users LIMIT 10000)抽样验证逻辑
实际业务中,“流失”不是二值判断,而是概率衰减过程。窗口函数能帮你锚定关键时间点,但阈值(7天?30天?)和周期对齐方式(自然日?滚动窗口?)必须贴合产品实际行为节奏,否则数字再准也没意义。

















