漏斗分析必须确认同一用户按顺序完成A→B→C路径,不能仅用CASE WHEN+SUM或GROUP BY user_id统计单事件;需用窗口函数ROW_NUMBER()按user_id分组排序打时序号,再用LEAD/LAG或自连接验证相邻步骤,同时注意时间精度与业务规则匹配。

漏斗分析不是简单统计“有多少人做了A、多少人做了B”,而是必须确认“同一个人是否按顺序完成了A→B→C”。直接用CASE WHEN + SUM会漏掉顺序和归属关系,结果完全不可信。
为什么不能只用GROUP BY user_id + CASE WHEN?
这种写法只回答“用户有没有做过某事”,不回答“是否按路径发生”。比如一个用户先pay再view,SUM(CASE WHEN event_name = 'view' THEN 1 END)和SUM(CASE WHEN event_name = 'pay' THEN 1 END)都会计数,但实际漏斗根本没走通。
常见错误现象:
- 转化率虚高(把跳步、倒序、跨天行为全算进漏斗)
- 同一用户被重复计入多个步骤(如多次
view导致step1人数膨胀) - 无法识别“有效路径”(比如
view → cart → pay中间隔了3天,业务上可能不算成功转化)
必须用窗口函数打时间序号
核心是给每个用户的每条行为打唯一时序号,这是后续判断路径的基础。只靠ORDER BY event_time不够,必须加PARTITION BY user_id。
正确写法示例:
SELECT
user_id,
event_name,
event_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time, event_id) AS rn
FROM events
WHERE event_name IN ('view', 'cart', 'pay');关键点:
-
event_id作为二级排序字段,解决event_time重复问题(否则ROW_NUMBER()可能打乱真实先后) - 过滤掉无关事件(如
fav),减少排序开销 - 别在子查询里
SELECT *再套窗口——先WHERE再OVER,性能差一个数量级
用LEAD/LAG匹配相邻步骤
有了序号后,LEAD()比自连接轻量得多,适合判断两步紧邻关系。
例如筛选“浏览后加购”的用户:
SELECT user_id
FROM (
SELECT
user_id,
event_name,
LEAD(event_name, 1) OVER (PARTITION BY user_id ORDER BY event_time, event_id) AS next_event
FROM events
WHERE event_name IN ('view', 'cart')
) t
WHERE event_name = 'view' AND next_event = 'cart';注意:
- 必须同时
PARTITION BY user_id和ORDER BY event_time, event_id,缺一不可 - 只写
ORDER BY event_time会导致A用户的view匹配到B用户的cart -
LEAD()返回NULL是正常行为(末尾行无下一行),不用额外IS NOT NULL过滤
三步以上漏斗慎用多重LEAD
要查view → cart → pay,嵌套两次LEAD()可实现,但可读性和维护性迅速下降:
LEAD(event_name, 1) ... AS next1, LEAD(event_name, 2) ... AS next2
更稳妥的做法:
- 用
ROW_NUMBER()生成序号后,自连接t1.rn = t2.rn - 1 AND t1.rn = t3.rn - 2 - ClickHouse用户直接用
windowFunnel(),它不依赖全排序,百万级数据也能秒出结果 - MySQL/PostgreSQL建议先用CTE提取每个用户的首步时间,再用
EXISTS逐层验证后续步骤是否存在且时间合规
真正容易被忽略的,不是语法怎么写,而是“时间精度是否一致”——日志里的event_time如果是秒级,而业务要求“5分钟内完成下一步”,就一定得用TIMESTAMPDIFF(SECOND, prev_time, curr_time) ,而不是粗暴比较日期或小时。

















