复购率需用嵌套查询计算:分子为至少购买2次的用户数,分母为全部付费用户数,推荐left join子查询方式并用nullif防除零;留存率须先通过子查询确定新用户首单日期,再关联判断n日内复购,注意时间字段口径、id统一及时区一致。

复购率:用子查询统计重复购买用户占比
复购率本质是「至少买过 2 次的用户数 / 所有付费用户数」。直接在 GROUP BY user_id 后加 HAVING COUNT(*) >= 2 只能筛出复购用户,但分母需要全量用户——必须用嵌套查询分离分子和分母逻辑。
常见错误是写成单层 COUNT(DISTINCT CASE WHEN cnt >= 2 THEN user_id END) / COUNT(DISTINCT user_id),这在 MySQL 8.0 之前不支持窗口函数时会报错,且无法处理订单时间跨月等场景。
推荐写法(兼容 MySQL/PostgreSQL):
SELECT
ROUND(
COUNT(DISTINCT t2.user_id) * 1.0 / NULLIF(COUNT(DISTINCT t1.user_id), 0), 4
) AS repurchase_rate
FROM orders t1
LEFT JOIN (
SELECT user_id
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2
) t2 ON t1.user_id = t2.user_id;
关键点:
-
NULLIF(..., 0)防止分母为 0 导致除零错误 - 外层用
t1表确保分母是全部下单用户(不是去重后的复购用户) - 若只算「指定时间段内」的复购率,两个
orders表都要加WHERE order_time BETWEEN ...,且子查询里不能漏掉时间过滤,否则会把历史老订单也计入
新用户留存率:用日期差 + 关联子查询定位首单用户
新用户留存率 = 「第 N 天还下单的新用户数」/「首单发生在 D 日的新用户数」。难点在于:必须先识别出每个用户的首单日期,再判断他们是否在 D+N 日再次下单。
错误做法是直接 WHERE order_time = MIN(order_time) OVER (PARTITION BY user_id) —— 这在 WHERE 子句里不合法,且无法用于关联后续行为。
正确思路:用子查询先生成「新用户首单表」,再 LEFT JOIN 原表查其 N 日后行为:
SELECT
ROUND(
COUNT(DISTINCT t2.user_id) * 1.0 / NULLIF(COUNT(DISTINCT t1.user_id), 0), 4
) AS retention_7d
FROM (
-- 所有新用户首单(按自然日计算,非滚动)
SELECT user_id, MIN(order_date) AS first_order_date
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY user_id
) t1
LEFT JOIN orders t2
ON t1.user_id = t2.user_id
AND t2.order_date = t1.first_order_date + INTERVAL '7' DAY
WHERE t1.first_order_date = '2024-01-01';
注意:
- PostgreSQL 用
INTERVAL '7' DAY,MySQL 用DATE_ADD(t1.first_order_date, INTERVAL 7 DAY) - 如果要算「7日内任意一天再次下单」,把
t2.order_date = ...改成t2.order_date BETWEEN t1.first_order_date + INTERVAL '1' DAY AND t1.first_order_date + INTERVAL '7' DAY - 首单日期必须限定范围(如
WHERE order_date >= '2024-01-01'),否则子查询可能拉取全表,性能崩盘
为什么不能全靠窗口函数?
有人试过用 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) 标记首单,再用 LEAD() 查下次下单时间——理论上可行,但实际容易翻车:
问题包括:
-
LEAD(order_date, 1)只能看紧邻下一次,无法灵活定义「7日内」这种区间条件 - 若用户首单后隔了 10 天才第二次下单,
LEAD会返回第 10 天的值,但你没法在同一个窗口里做「是否 ≤ 7」的布尔判断再聚合 - 当数据量大、用户行为稀疏时,窗口函数比关联子查询更吃内存,某些 Hive/SparkSQL 版本会 OOM
时间精度和去重陷阱
复购和留存都对时间字段敏感,但业务口径常被忽略:
比如「新用户」定义是「首次支付成功时间」还是「首次创建订单时间」?orders 表里如果有 pay_time 和 create_time 两个字段,必须统一用 pay_time,否则未支付订单会污染首单识别。
另一个坑是用户 ID 不唯一:微信小程序可能用 openid,App 用 device_id 或 user_id,如果没做 ID 映射打通,直接算出来的留存率会偏低。
最后提醒:所有涉及日期的比较,务必确认时区一致。数据库用 UTC、业务看板用东八区,'2024-01-01' 在两边可能对应不同物理日。











