阶段转化率指用户从漏斗上一阶段进入下一阶段的比例,即前一阶段用户中完成后一阶段的人数占比;需用嵌套子查询或cte分别统计各阶段人数后做除法,避免join导致的重复计数、空值和时序问题。

什么是漏斗模型里的“阶段转化率”
漏斗模型本质是统计用户在一系列有序行为中,从上一阶段进入下一阶段的比例。比如:访问首页 → 加入购物车 → 提交订单 → 支付成功,每个箭头对应一个转化率。关键不是“总人数”,而是“前一阶段用户中,有多少人完成了后一阶段”。嵌套查询在这里的作用,是把各阶段的独立计数和关联关系显式表达出来,避免用多个JOIN引入重复行或丢失中间阶段的空值。
用子查询分别统计各阶段人数再做除法
最直接、也最容易控制逻辑的方式:每个阶段用一个独立的SELECT COUNT(*)子查询,然后在外部查询里做除法。这样写清楚、易调试、不依赖表结构是否支持自连接。
示例(以 PostgreSQL 为例):
SELECT
ROUND(100.0 * cart_cnt / visit_cnt, 2) AS "首页→加购",
ROUND(100.0 * order_cnt / cart_cnt, 2) AS "加购→下单",
ROUND(100.0 * pay_cnt / order_cnt, 2) AS "下单→支付"
FROM (
SELECT
(SELECT COUNT(*) FROM events WHERE event_type = 'page_view' AND page = 'home') AS visit_cnt,
(SELECT COUNT(*) FROM events WHERE event_type = 'add_to_cart') AS cart_cnt,
(SELECT COUNT(*) FROM events WHERE event_type = 'submit_order') AS order_cnt,
(SELECT COUNT(*) FROM events WHERE event_type = 'payment_success') AS pay_cnt
) t;
注意几个实操点:
- 所有子查询必须返回单个标量值,否则会报错
more than one row returned by a subquery used as an expression - 用
100.0而不是100做乘法,避免整数除法截断(如 MySQL/PostgreSQL 中3/5 = 0) - 如果某阶段人数为 0(比如还没人支付),除零会出错;生产环境建议用
NULLIF(cart_cnt, 0)包裹分母
用 WITH 子句提升可读性与复用性
当阶段变多(比如 5 步以上)、或需要对同一阶段加时间窗口/用户去重时,把各阶段逻辑拆成 CTE 更清晰,也方便后续扩展(比如加同期对比)。
示例(带用户去重):
WITH stage1 AS (SELECT COUNT(DISTINCT user_id) AS cnt FROM events WHERE event_type = 'page_view' AND page = 'home'),
stage2 AS (SELECT COUNT(DISTINCT user_id) AS cnt FROM events WHERE event_type = 'add_to_cart'),
stage3 AS (SELECT COUNT(DISTINCT user_id) AS cnt FROM events WHERE event_type = 'submit_order')
SELECT
ROUND(100.0 * s2.cnt / NULLIF(s1.cnt, 0), 2) AS "visit→cart",
ROUND(100.0 * s3.cnt / NULLIF(s2.cnt, 0), 2) AS "cart→order"
FROM stage1 s1, stage2 s2, stage3 s3;
这里的关键差异:
-
WITH比嵌套子查询更易维护,尤其当你需要在多个地方引用同一阶段结果时 -
COUNT(DISTINCT user_id)是真实漏斗的常见需求,但要注意它比COUNT(*)慢不少,大数据量下需确认索引覆盖event_type和user_id - 多个 CTE 用逗号分隔,最后主查询用
FROM直接并列引用——这不是笛卡尔积,因为每个 CTE 只返回一行
为什么不用 JOIN 做漏斗?容易踩什么坑
有人试图用 LEFT JOIN 把各阶段行为连成一张宽表(比如 user_id 关联首页访问、加购、下单记录),再按用户聚合。这在理论上可行,但实践中极易出错:
- 一个用户多次加购,会导致该用户在宽表里出现多行,
COUNT(*)被放大,转化率虚高 - 如果某阶段无记录(如用户没下单),
LEFT JOIN后对应字段为NULL,但COUNT(*)仍会计数,必须改用COUNT(stage2.user_id)才对 - 时间顺序难保证:用户可能先下单再加购(数据乱序或埋点异常),JOIN 不校验事件时间戳,漏斗就失去意义
- 阶段一多(>4),JOIN 层级深,执行计划变慢,且难以加
WHERE条件过滤中间阶段
所以除非你明确需要分析“完成全路径的用户特征”,否则别用 JOIN 做基础漏斗统计。
真正麻烦的从来不是怎么写 SQL,而是定义清楚每个阶段的业务语义——比如“提交订单”是指前端按钮点击,还是后端创建订单记录?时间范围是否对齐?这些不厘清,再漂亮的 SQL 也算不准。










