lead()和lag()必须配合partition by user_id与order by login_date(建议加id防抖动),否则逻辑错误;次日留存需用timestampdiff(day, login_date, next_login) = 1判断,分母应为观察期内首次登录用户数。

LEAD() 和 LAG() 必须配 PARTITION BY + ORDER BY,否则结果错乱
单独写 LEAD(login_date) OVER (ORDER BY login_date) 会让所有用户混排,张三的登录日可能被李四的日期“顶替”,MySQL 8.0+ 直接报错,PostgreSQL 虽不报错但结果不可复现。这不是性能问题,是逻辑崩塌。
真正有效的写法必须同时满足:
-
PARTITION BY user_id:确保只在同一个用户内找“下一次”或“上一次”行为 -
ORDER BY login_date:显式声明顺序;若存在秒级并发(比如同天多条记录),建议追加id防止排序抖动,例如ORDER BY login_date, id - 不能省略
ORDER BY—— 即使表有主键或索引,窗口函数也不保证默认顺序
算次日留存别只看 LEAD() 是否非空,得做日期差判断
WHERE next_login IS NOT NULL 统计的是“后续有行为的用户”,不是“次日就回来的用户”。真实留存要验证时间间隔是否刚好为 1 天。
正确做法是用 TIMESTAMPDIFF(DAY, login_date, next_login) = 1:
- 别用
DATE_ADD(login_date, INTERVAL 1 DAY) = next_login:跨月时会出错(如 '2026-01-31' + 1 DAY ≠ '2026-02-01') - 更别用字符串截取或
DAYOFWEEK():它们无法识别实际时间跨度 -
LEAD(login_date) OVER (PARTITION BY user_id ORDER BY login_date)只负责取值,判断逻辑必须另写
LAG() 不只是“取上一行”,它能锚定首次行为和识别断连
很多人把 LAG() 当成可有可无的辅助函数,其实它在留存分析里承担两个关键任务:
- 识别首次登录:
LAG(login_date) OVER (PARTITION BY user_id ORDER BY login_date)返回NULL的行,就是该用户的首条记录 - 支撑多日归因:比如算 7 日留存,用
LAG(login_date, 6)拿到 7 天前的日期,再比对是否连续活跃 -
LAG()默认返回NULL,若需占位值(如 0 或 'N/A'),必须显式传第三个参数:LAG(login_date, 1, '1970-01-01')
分母必须是“观察期内新用户”,不是当天活跃用户总数
直接 COUNT(DISTINCT user_id) 当分母,会把老用户也计入,导致留存率被严重稀释。真正的分母是“在指定时间段内首次登录的用户数”。
实现路径分两步:
- 先用窗口函数打标:
MIN(login_date) OVER (PARTITION BY user_id)得到每个用户的first_login - 再过滤:
WHERE first_login BETWEEN '2026-05-01' AND '2026-05-07' - 原始数据含脏数据(如
'0000-00-00'、NULL)时,务必前置清洗:WHERE login_date >= '2026-01-01' AND login_date IS NOT NULL
漏掉“首次”这个前提,整个留存率就失去业务意义——你算的不是新用户留存,而是所有用户的行为密度。











