漏斗转化必须先按user_id去重再判别用户行为,而非直接group by event_type;否则会导致分母非同一用户池、转化率失真甚至超100%。

漏斗转化不能只用 GROUP BY event_type
直接 GROUP BY event_type 统计各环节人数,结果完全不可用于计算转化率。它把每个事件当独立样本处理:一个用户刷了 5 次 view、1 次 add_to_cart,就会被算作 5 个“曝光用户”和 1 个“加购用户”,但分母根本不是同一组人。转化率公式 uv_cart / uv_view 的分母 uv_view 必须是“所有至少发生过一次 view 的去重用户”,而不是“所有 view 行数”。
常见错误现象包括:uv_add_to_cart > uv_view(说明没去重)、转化率超过 100%、不同日期结果波动剧烈却查不出原因。
- 必须先按
user_id去重,再判断该用户是否达成各环节 -
CASE WHEN中ELSE 0不能省——否则SUM()会跳过NULL,导致总数偏低 - 用
NULLIF(uv_view, 0)包裹分母,避免除零报错中断整条 SQL
正确写法:单次扫描 + DISTINCT + 多 CASE WHEN
核心是把“用户池”固定下来,再在同一行里做多条件判别。不是分组,是判别;不是统计行,是统计人。
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')
AND event_time >= '2026-10-01'
AND event_time <p>关键点:</p>
-
DISTINCT user_id, event_type是去重底线:防止同一用户多次触发同一事件被重复计数 - 时间过滤必须放在子查询内,否则外层
SUM会漏掉跨周期行为 - 若原始
event_type含空格或大小写不一致(如'add_to_cart '),CASE WHEN完全不匹配——务必提前清洗:TRIM(LOWER(event_type))
Hive 环境要绕开 DISTINCT ON
Hive 不支持 DISTINCT ON,也不能在子查询里直接 SELECT DISTINCT user_id, event_type——因为 Hive 会把它重写成 MapReduce 任务,DISTINCT 在 Reduce 阶段才生效,中间可能膨胀出大量冗余数据。
稳妥做法是用 ROW_NUMBER() 取每个用户首条有效行为:
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
FROM (
SELECT user_id, event_type
FROM (
SELECT
user_id,
TRIM(LOWER(event_type)) AS event_type,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time, event_id) AS rn
FROM events
WHERE user_id IS NOT NULL
AND event_type IN ('view', 'add_to_cart', 'purchase')
AND event_time >= '2026-10-01'
AND event_time <p>注意:</p>
-
ORDER BY event_time, event_id是防时间重复的刚需,否则rn = 1结果不确定 - 不要在最外层再套一层
GROUP BY user_id——这又退化回单事件统计逻辑 - 如果业务要求“用户只要做过任一环节就算入漏斗”,那
rn = 1是对的;但如果要保留用户全部环节行为(比如同时有 view 和 cart),就得去掉rn = 1,改用GROUP BY user_id后聚合判断
高并发下性能卡点其实在 WHERE 和索引
漏斗 SQL 慢,90% 不是因为 CASE WHEN,而是子查询扫描了全表。尤其在事件表日增千万级时,没索引的 WHERE user_id IS NOT NULL AND event_type IN (...) AND event_time BETWEEN ... 会直接触发全表扫描。
必须建的复合索引(以 MySQL 为例):
CREATE INDEX idx_events_time_type_uid ON events (event_time, event_type, user_id);
理由:
-
event_time放首位——漏斗几乎总是按天/周切片,范围查询效率最高 -
event_type次之——IN条件能走索引下推(Index Condition Pushdown) -
user_id放最后——用于DISTINCT或PARTITION BY时减少回表 - 避免单独建
(user_id)索引——对漏斗场景无效,还拖慢写入
真正容易被忽略的是:这个索引对 event_time 的选择性高度敏感。如果查询跨月(如 BETWEEN '2026-01-01' AND '2026-12-31'),即使有索引也会扫几百万行。生产中应强制按自然日分区,或在应用层拆成多个单日查询合并结果。










