LAG()计算沉默天数必须加PARTITION BY user_id,否则跨用户取值导致结果错误;需升序排序、COALESCE处理NULL,并结合历史活跃度分位与生命周期阶段设定差异化阈值。

LAG() 计算沉默天数必须加 PARTITION BY user_id
直接写 LAG(event_time) 会跨用户取值,比如用户 A 的最后行为被当成用户 B 的“上一次”,结果全错。必须明确分区:LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)。
常见错误现象:沉默天数出现负值、极大值(如几万天),或大量用户显示相同沉默天数——基本是漏了 PARTITION BY 导致窗口混用。
使用场景:用户最后一次行为距当前时间的间隔,是判断流失的起点;但注意,这个“当前时间”通常不是 NOW(),而是分析快照日(如 CURRENT_DATE - INTERVAL '1 day'),否则未当日上报的行为会被误判。
ORDER BY 升序 + COALESCE 处理首行 NULL
ORDER BY event_time 必须升序,这样 LAG() 取的是前一次行为时间,再用 DATEDIFF('day', LAG(), event_time) 才是本次与上次的间隔。降序会算成“距下次多久”,不适合流失预警。
首条记录没有前驱,LAG() 返回 NULL,直接参与 DATEDIFF 会导致整行结果为 NULL,后续 WHERE last_gap > 30 会跳过该用户,分母丢失。
实操建议:
- 用
COALESCE(DATEDIFF('day', LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time), event_time), 999)把首行兜底为一个明显异常的大值 - 或更稳妥地,在外层先过滤掉
event_time IS NOT NULL且LAG(event_time) IS NOT NULL的行,确保只对有历史对比的记录打标 - 某些引擎(如 Redshift)要求
ORDER BY字段非NULL,空时间戳会导致该行被窗口函数跳过,需提前清洗
别用 ROWS BETWEEN UNBOUNDED PRECEDING 算活跃基线
单纯看“距今 15 天没登录”不靠谱:高频用户停 3 天就算风险,低频用户停 30 天才需关注。得结合用户自身历史节奏。
错误做法:AVG(1) OVER (PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) —— 这算的是累计活跃密度,不是滚动节奏。
正确做法:
- 先定义观察窗口,如过去 60 天:
event_time >= CURRENT_DATE - INTERVAL '60 days' - 用
COUNT(*)和COUNT(DISTINCT DATE_TRUNC('day', event_time))分别算总行为数和活跃天数,再除得日均行为频次 - 用
NTILE(5) OVER (ORDER BY avg_daily_actions)把用户按历史活跃度五等分,不同分位设置不同沉默阈值(如 Top 20% 用户设 7 天,Bottom 20% 设 45 天)
ROWS BETWEEN 比 RANGE BETWEEN 更安全:后者按时间值拉数据,若用户行为稀疏(比如隔两周才一次),RANGE 可能把两个月前的记录也卷进来,均值失真。
组合多个窗口指标输出风险等级,避开单一阈值陷阱
没有万能阈值。同一 last_gap = 15,对电商用户可能是高危,对 SaaS 工具用户只是周末休。
实操建议:
- 至少叠加两个维度:沉默天数 + 历史活跃分位 + 最近行为类型(如最后行为是“提交订单”还是“浏览商品详情页”)
- 用
CASE WHEN last_gap > threshold_by_tier AND last_action_type IN ('logout', 'cancel_subscription') THEN 'high_risk'...分级 - 避免在窗口函数里嵌套太多逻辑,把基础指标(
last_gap、avg_daily_actions、last_action_type)先算出,再在外层CASE组合——可读性强,也方便下游复用
真正容易被忽略的是:用户生命周期阶段。新用户注册后第 3 天沉默,和老用户连续使用 2 年后突然沉默,业务含义完全不同。必须把 first_active_date 作为独立字段参与判断,不能只依赖最近行为。

















