group by无法计算漏斗转化率,因其仅统计各环节独立去重人数,无法保证后步用户是前步子集;需用case when+sum+去重子查询,以用户为单位条件聚合,并注意trim、nullif及有序漏斗的时序处理。

GROUP BY 本身不能直接算漏斗转化率
直接 SELECT event_type, COUNT(DISTINCT user_id) FROM events GROUP BY event_type 得到的只是各环节独立去重人数,不是漏斗。比如 view 有 1000 人、add_to_cart 有 800 人,但这 800 人里可能只有 600 人来自那 1000 个 view 用户——你根本不知道分母是谁。漏斗要求“后一步用户必须是前一步用户的子集”,而 GROUP BY 没法在单行中表达这种跨步骤归属关系。
CASE WHEN + SUM + 去重子查询才是正解
核心是把统计单位从“事件行”切换到“用户”,再用条件聚合一次性判别每个用户是否达成各环节:
- 先用子查询
SELECT DISTINCT user_id, event_type FROM events构建干净用户池(避免同一用户多次行为膨胀计数) - 在外层用多个
CASE WHEN event_type = 'xxx' THEN 1 ELSE 0 END判断该用户是否命中某环节 - 用
SUM()汇总,确保所有环节人数都基于同一份DISTINCT user_id -
ELSE 0必须显式写出,否则SUM会跳过NULL,导致总数偏小 - 除零要用
NULLIF(denominator, 0)包裹,否则整条 SQL 报错
示例(PostgreSQL / MySQL 8.0+):
SELECT
SUM(CASE WHEN event_type = 'view' THEN 1 ELSE 0 END) AS uv_view,
SUM(CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END) AS uv_cart,
SUM(CASE WHEN event_type = 'purchase' THEN 1 ELSE 0 END) AS uv_purchase,
ROUND(100.0 * uv_cart / NULLIF(uv_view, 0), 2) AS rate_view_to_cart
FROM (
SELECT DISTINCT user_id, event_type
FROM events
WHERE user_id IS NOT NULL
AND event_type IN ('view', 'add_to_cart', 'purchase')
) t;
Hive 等不支持 DISTINCT ON 的场景要改写
Hive 不支持 DISTINCT ON,也不能直接 SELECT DISTINCT user_id, event_type(可能丢掉用户多事件中的关键环节)。必须用窗口函数保序取样:
- 用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time, event_id)给每个用户行为打序号 - 外层
WHERE rn = 1取每个用户首条行为(或按业务选最相关的一条) - 若需保留用户全部环节,就得用
COLLECT_SET(event_type)聚合成数组再判断,但性能差、可读性低 - 原始数据中
event_type带空格(如'add_to_cart ')会导致CASE WHEN完全不匹配,务必提前TRIM()
当漏斗依赖严格时序时,CASE WHEN 就失效了
上面方法只解决“无序漏斗”(即只要用户做过 A 和 B,不管先后),但真实业务常要求“有序漏斗”:必须 A 发生在 B 之前,且中间不能隔太久。这时:
-
CASE WHEN完全无法表达时间先后,必须引入ROW_NUMBER()或LEAD()/LAG() - 排序必须带二级键:
ORDER BY event_time, event_id,否则同秒内多事件顺序不可控 - 用
LEAD(event_type, 1) OVER (PARTITION BY user_id ORDER BY event_time, event_id)才能抓到“某次 view 后紧跟着 cart”的记录 - 时间窗口需业务定义(如 30 分钟内),不能只靠排序,得加
event_time BETWEEN ...过滤
真正难的不是写 SQL,而是明确业务规则:这个漏斗到底认不认倒序?允不允许穿插其他事件?时间跨度卡多严?这些没定清楚,代码写得再漂亮也和业务对不上。










