漏斗统计不能只用COUNT(*),因为其统计的是事件次数而非用户数,导致用户身份未对齐、步骤顺序未校验、跨步去重缺失;必须先按user_id分组打布尔标签(如MAX(CASE WHEN...THEN 1 ELSE 0 END)),再逐级相乘计算各环节用户数,确保转化率分子分母基于同一用户池。

漏斗统计为什么不能只用 COUNT(*)
直接对每步事件用 COUNT(*) 算总数再手动除,看似简单,但实际会漏掉关键逻辑:用户身份未对齐、步骤顺序未校验、跨步去重未处理。比如“注册→下单→支付”三步,若某用户注册两次、下单一次、支付零次,用裸 COUNT(*) 会把注册数虚高,且无法判断是否同一用户完成连续动作。
必须用子查询或 CTE 预先标记用户路径
核心是先识别出“走到哪一步的用户”,而不是“发生了多少次事件”。典型做法是用 MAX() 或 BOOL_OR()(PostgreSQL)/ MAX(CASE WHEN ... THEN 1 ELSE 0 END)(MySQL/Oracle)为每个用户打标签:
- 用
user_id分组,对每步事件是否存在做布尔标记 - 按步骤顺序定义“是否完成该步及之前所有步”,即“注册→下单→支付”要求前序步骤都存在
- 避免用
SUM()累加事件次数——那统计的是行为频次,不是用户留存
示例(MySQL):
WITH user_steps AS (
SELECT user_id,
MAX(CASE WHEN event = 'register' THEN 1 ELSE 0 END) AS has_register,
MAX(CASE WHEN event = 'order' THEN 1 ELSE 0 END) AS has_order,
MAX(CASE WHEN event = 'pay' THEN 1 ELSE 0 END) AS has_pay
FROM events
WHERE event IN ('register', 'order', 'pay')
GROUP BY user_id
),
funnel AS (
SELECT
COUNT(*) AS total_users,
SUM(has_register) AS reg_users,
SUM(has_register * has_order) AS order_users,
SUM(has_register * has_order * has_pay) AS pay_users
FROM user_steps
)
SELECT
reg_users / total_users AS reg_rate,
order_users / reg_users AS order_rate,
pay_users / order_users AS pay_rate
FROM funnel;窗口函数不适合直接算留存率,但可辅助归因分析
ROW_NUMBER() 或 RANK() 适合排序、分层、找首单时间,但不解决“用户是否完成路径”的布尔判断问题。强行用 LAG() 比较前后事件类型,容易因数据乱序、重复、缺失导致误判。真正需要的是聚合后的用户级状态快照,不是行级时序推导。
- 窗口函数在漏斗中更适合计算“各步骤平均耗时”“首步到末步中位数间隔”等衍生指标
- 若原始表无
event_time,或存在多设备登录、事件延迟上报,LEAD()/LAG()结果不可靠 - MySQL 5.7 及更早版本不支持窗口函数,硬套会报错
ERROR 1064
Oracle 和 MySQL 在 NULL 处理上差异影响结果精度
AVG() 和 COUNT() 对 NULL 的默认行为不同:MySQL 的 AVG() 自动跳过 NULL,但 Oracle 的 AVG() 同样如此;真正易错的是用 COUNT(col) 而非 COUNT(*) —— 前者忽略该列值为 NULL 的行,后者统计所有非空行。漏斗中若某步事件字段为空(如 pay_amount IS NULL),用 COUNT(pay_amount) 会少计“已支付但金额未回传”的用户。
- 统一用
COUNT(*)统计用户数,配合CASE WHEN控制分子逻辑 - Oracle 中
NVL()可显式补0,但不如布尔标记清晰 - 别依赖
DECODE()做多层嵌套判断,可读性差且难调试
最常被忽略的一点:漏斗统计必须基于会话(session)或设备+用户组合去重,而非原始日志行。原始事件表里一个用户可能产生几十条曝光日志,但只算一个注册用户——这层清洗必须前置,不能指望聚合函数自动识别。

















