LAG()可高效计算用户上次登录间隔,需PARTITION BY user_id并ORDER BY login_time;首行返回NULL应处理;活跃周期需结合gap判断与累积计数生成周期编号;MySQL 5.7以下需升级或用自连接替代。

用 LAG() 算上一次登录时间,再减出间隔
核心思路是:把用户每次登录按时间排序,拉出前一行的 login_time,和当前行相减得到距上次登录的天数(或秒数)。LAG() 是最直接、开销最小的方式,比自连接或子查询快得多。
常见错误是没加 PARTITION BY user_id,导致跨用户计算间隔——比如用户 A 的最后一次登录,被拿来减用户 B 的第一次登录,结果完全失真。
- 必须按
user_id分组,再按login_time排序:LAG(login_time) OVER (PARTITION BY user_id ORDER BY login_time) - 时间相减结果类型依赖数据库:PostgreSQL 返回
interval,MySQL 8.0+ 返回秒数(用TIMESTAMPDIFF(SECOND, ..., ...)更稳),SQLite 需转为 julian day - 首条记录的
LAG()值为NULL,别忘了用COALESCE(..., 0)或过滤掉
识别“活跃周期”得先定义什么是“断连”
没有统一标准:7 天不登录算流失?14 天?还是按业务节奏(比如电商大促后 3 天内回访才算延续)?窗口函数只负责算间隔,判断“是否属于同一活跃周期”是后续逻辑。
典型做法是加一列标记:如果距上次登录 ≤ N 天,就继承上一个周期 ID;否则新建周期。这需要 ROW_NUMBER() 配合条件累积计数,不能只靠 LAG()。
- 用布尔值转整数累加:
SUM(CASE WHEN gap_days > 7 THEN 1 ELSE 0 END) OVER (PARTITION BY user_id ORDER BY login_time) - 这个累加值就是“活跃周期编号”,相同编号的记录属于同一次连续活跃
- 注意:窗口排序必须严格一致,否则周期会被拆断;
ORDER BY login_time, id可防时间相同时序错乱
LEAD() 和 LAG() 别混用,场景完全不同
LAG() 看过去,适合算“已发生的间隔”;LEAD() 看未来,适合算“下次登录还要等多久”。检测活跃周期只关心“上次啥时候来过”,所以基本不用 LEAD()。
误用 LEAD() 的典型表现:查出“用户下次登录在 5 天后”,但实际他再也没来过——LEAD() 对末尾记录返回 NULL,容易被当成“无限等待”,误导周期判断。
- 只有做预测性分析(如预警流失)时才考虑
LEAD() - 若强行用
LEAD()反推上一次间隔,需配合ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING,反而绕远且难读 - 性能上二者无差异,但语义错位会增加维护成本
MySQL 5.7 不支持窗口函数?得换思路
MySQL 5.7 及更早版本没有 LAG(),硬套窗口语法会报错:ERROR 1064 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'(常因子查询嵌套触发)。
替代方案是变量法,但极易出错:变量执行顺序不保证,多用户并发时可能串数据,且无法在视图或子查询中复用。
- 安全做法:升级到 MySQL 8.0+,这是最省心的解
- 临时 workaround:用自连接 +
NOT EXISTS找上一条记录,但数据量过万就明显变慢 - 千万别信“
@prev := ...在ORDER BY后就一定可靠”——官方文档明确说变量赋值顺序未定义

















