漏斗转化率不能只用COUNT+GROUP BY,因漏斗是用户有序行为序列,直接计数会重复计算同一用户的多次行为,导致分母膨胀、转化率失真;ClickHouse用retention()函数按时间顺序判断用户各步达成情况,PG/MySQL则需窗口函数+条件聚合模拟,核心是用户级去重与时间窗口对齐。

漏斗转化率为什么不能只用 COUNT + GROUP BY?
因为漏斗本质是用户行为的**有序序列**,不是独立事件。直接对每个步骤 COUNT 会重复计算同一用户的多次行为,导致分母膨胀、转化率失真。比如一个用户完成“浏览→加购→下单”三次,COUNT(*) 会把他在“浏览”步骤算作 3 次,但实际只应算作 1 个起始用户。
ClickHouse 中用 retention() 实现多步漏斗(21.1+)
retention() 是专为漏斗设计的布尔数组聚合函数,它按时间顺序判断用户是否在各阶段出现过,返回长度固定的布尔数组,每项对应一步是否达成。
- 必须配合
ORDER BY user_id, event_time使用,否则结果不可靠 - 参数是多个布尔表达式,顺序即漏斗步骤顺序,例如:
retention(event_type = 'view', event_type = 'cart', event_type = 'pay') - 返回数组如
[1,1,0]表示该用户完成了前两步但未完成第三步 - 后续用
sum(x[1])统计第一步用户数,sum(x[2])统计第二步用户数,再做除法得转化率
示例:
SELECT
sum(r[1]) AS view_uv,
sum(r[2]) AS cart_uv,
round(sum(r[2]) / sum(r[1]), 4) AS view_to_cart_rate,
sum(r[3]) AS pay_uv,
round(sum(r[3]) / sum(r[1]), 4) AS view_to_pay_rate
FROM (
SELECT user_id, retention(
event_type = 'view',
event_type = 'cart',
event_type = 'pay'
) AS r
FROM user_events
WHERE event_time >= '2026-09-01'
GROUP BY user_id
)
PostgreSQL / MySQL 中用窗口函数 + 条件聚合模拟漏斗
没有原生漏斗函数时,核心思路是:先按用户打标(是否完成某步),再聚合统计。关键在避免重复计数,所以要用 DISTINCT ON(PG)或 ROW_NUMBER()(通用)取每个用户的首次行为。
- 先用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time)标记每用户最早行为时间 - 再用
MAX(CASE WHEN ... THEN 1 ELSE 0 END)对每个用户判别是否走过某步(不是 COUNT) - 最后在外层用
SUM()统计达标用户数,而非行为数 - 注意:漏斗步骤间的时间约束需显式加在 WHERE 或子查询中,否则跨天行为会被误连
示例(PG):
SELECT
COUNT(*) FILTER (WHERE viewed = 1) AS view_uv,
COUNT(*) FILTER (WHERE carted = 1) AS cart_uv,
ROUND(AVG(carted::numeric), 4) AS view_to_cart_rate
FROM (
SELECT
user_id,
MAX((event_type = 'view')::int) AS viewed,
MAX((event_type = 'cart')::int) AS carted
FROM (
SELECT DISTINCT ON (user_id) user_id, event_type
FROM user_events
WHERE event_time >= '2026-09-01'
ORDER BY user_id, event_time
) first_events
GROUP BY user_id
) u;
精度与性能的现实权衡点
漏斗统计最容易被忽略的是**时间窗口对齐**和**用户去重粒度**。比如“注册→付费”漏斗,如果注册和付费时间跨自然日,但没限定最大间隔(如7天),就会把长期沉睡用户计入,拉低转化率;又比如用 user_id 去重,但业务实际按设备或手机号归因,结果就不可信。
另外,retention() 在 ClickHouse 中虽快,但要求数据已按 user_id, event_time 排序,否则需额外 ORDER BY 开销;而 PG/MySQL 方案虽灵活,但嵌套层级一多,执行计划容易退化——这时候不如提前物化中间表,把每用户各步状态存成宽表字段。

















