用lag()函数按user_id分区、activity_time排序计算行为间隔,差值超90天且无后续行为即判定流失;须基于原始日志而非last_active_date快照,避免更新延迟导致误判。

直接结论:用 LAG() 算上次活跃时间,再和当前时间比差值,大于阈值(如90天)即判定为流失;但必须配合 PARTITION BY user_id 和 ORDER BY activity_time,否则结果全错。
为什么不能只查 last_active_date 字段
很多表里自带 last_active_date 字段,但它是静态快照,更新不及时或被批量覆盖。真实流失判断必须基于原始行为日志——比如用户昨天刚登录,但系统还没来得及刷新该字段,你一查就误判为“已流失”。
- 依赖字段更新逻辑不可控,尤其跨服务写入时容易延迟数小时甚至一天
- 无法区分“真流失”和“只是今天没来”,缺少时间序列上下文
- 一旦字段为空或为远古日期(如
'1970-01-01'),直接参与计算会污染整个结果集
LAG() 计算间隔的正确写法
核心是把用户行为按时间拉成序列,再看“这次和上次之间隔了多久”。错误写法常漏掉分区或忽略 NULL。
- 必须写
LAG(activity_time) OVER (PARTITION BY user_id ORDER BY activity_time)—— 漏掉PARTITION BY会导致用户 A 的最后一次行为减去用户 B 的第一次行为 - 首条记录的
LAG()返回NULL,不能直接相减,得用WHERE prev_time IS NOT NULL过滤,或用COALESCE(prev_time, '1970-01-01')补默认值(但补值要慎选,避免误判) - 时间相减结果类型因库而异:
MySQL 8.0+返回秒数,建议统一用TIMESTAMPDIFF(DAY, prev_time, activity_time);PostgreSQL返回interval,可直接activity_time - prev_time > INTERVAL '90 days'
如何定义“流失用户量”并统计
流失不是单点状态,而是基于窗口的判断:某用户在最近 N 天内无任何行为,才计入当期流失量。常见误区是把“所有历史中最后一次行为距今超90天”的人全算进去,这会重复统计、虚高数字。
- 先限定统计范围:只看
activity_time >= DATE_SUB(CURDATE(), INTERVAL 180 DAY)的行为,保证有足够前置数据 - 对每个用户取最大
activity_time(即最后活跃日),再和当前日期比:CURDATE() - MAX(activity_time) > 90 - 更严谨的做法是:用
LAG()算出每次行为的间隔,再标记“本次间隔 > 90 且下一次行为为空”,即末次行为后彻底断连——这能排除“只是还没来得及下次登录”的干扰 - 注意时区:如果日志是 UTC,而业务按北京时间算流失,得先
activity_time AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Shanghai'再比较
容易被忽略的边界情况
真正上线跑批时,最常翻车的不是主逻辑,而是这些细节:
- 用户有重复行为(同秒多次),
LAG()可能拉到同一秒的上一条,差值为 0 —— 应先DISTINCT ON (user_id, DATE(activity_time))或用GROUP BY user_id, DATE(activity_time)去重 - 数据库版本限制:
MySQL 5.7不支持窗口函数,强行用会报错;必须升级或改用自连接(性能差 5–10 倍) - 活跃行为定义模糊:是登录?下单?还是任意埋点?必须和产品对齐,不能默认所有
event_type都算,否则“页面曝光”刷屏也会被当活跃










