漏斗转化率算不准,十有八九是没搞明白“用户级路径判定”这件事。说白了,就是得拿 user_id 去一个个看每个用户到底走到了哪一步,而不是简单地数一数事件行数再除一下完事。

最常见的一个坑,就是“独立统计后硬除”。比如,先算出有1000个用户访问了页面,800个用户加了购物车,然后直接拿800除以1000,算出转化率80%。听起来很顺,但问题在于——这800个加购的用户,真的都是从那1000个访问用户里来的吗?他们可能包含了新用户、跨天回来的用户,甚至是不小心重复计算的ID。真正的漏斗转化,分母必须是“上一步那群人里的有效分子”。也就是说,能走到第二步的人,必须先得在第一步的集合里。
具体来说:
- 第一步(比如
view_landing)的分母,是给定时间窗口内去重后的user_id数量。 - 第二步(比如
add_to_cart)的分母,就得是同一个时间窗口内,既触发了view_landing又触发了add_to_cart的user_id数量。 - 必须用
INNER JOIN或者WHERE user_id IN (SELECT ...)这种方式,把交集显式地约束出来,不能光靠时间范围“碰巧”重叠。
用 MAX(CASE WHEN ...) 做用户级标记,最稳
相比嵌套子查询或者多次 JOIN,对每个 user_id 打一个标记,看它是否完成了某个环节,这种做法更清晰、更易读,而且对数据库也更友好。核心思路就是:先按 user_id 分组,然后用条件聚合生成一个布尔标记。
- 比如:
MAX(CASE WHEN event_name = 'view_landing' THEN 1 ELSE 0 END),返回1就表示这个用户至少做过一次这个动作。 - 这种写法比
EXISTS子查询更容易调试,也避免了 MySQL 里相关子查询性能差的老毛病。 - 最关键的一点:所有环节的时间范围必须统一。比如都限定
event_time >= '2026-06-16' AND event_time < '2026-06-23',否则分母就失去了可比性,整个漏斗就失真了。
时间顺序和窗口限制,必须显式处理
一个用户先支付后浏览,用 CASE WHEN 也会标记成“完成了两步”,但这显然不是真正的漏斗路径。真实路径要求后一步必须发生在前一步之后,而且间隔要在业务能容忍的窗口内(比如1小时,或者7天)。
- 可以用
LAG(event_name) OVER (PARTITION BY user_id ORDER BY event_time)拿到上一个事件类型,然后再用WHERE event_name = 'add_to_cart' AND prev_event = 'view_landing'来筛选。 - 如果要求“加购必须在浏览后30分钟内”,就得加一个
event_time - prev_time < INTERVAL '30 minutes'的条件。 - 注意:
LAG()对每个用户的第一行会返回NULL,这个不需要额外过滤,但排序必须严格按event_time,不能只按日期。
除零和整数截断,是线上事故的高发点
线上生产环境里的 SQL,如果漏掉 NULLIF 或者用了整数除法,结果就可能直接变成 NULL 或者 0,而且这种错误很难被监控发现。
- 分母必须包一层
NULLIF(denominator, 0),否则pay_cnt / order_cnt在没人下单的时候,直接报错给你看。 - 乘法时要乘 100.0 而不是 100。比如在 PostgreSQL/MySQL 里,
3/5 = 0,但3*100.0/5 = 60.0,这个差别影响很大。 - 如果某个环节的人数为0,转化率应该显示为
NULL或者明确标注“无数据”,而不是直接跳过这一行。
还有一个容易被忽略的点:同一个用户在单次会话里重复触发同一个事件(比如多次点击注册按钮),会导致第一步的人数虚高。所以,必须确认去重逻辑是在事件层(用 COUNT(DISTINCT user_id))还是会话层(用 COUNT(DISTINCT session_id))。口径不一致,整个漏斗就失效了。
