找出曾经连续 3 个月、每个月都至少消费过一次,但在最近 30 天内没有任何消费和登录记录的用户占比
大厂 SQL 面试真题:流失的高价值用户占比分析
1. 题目背景
在电商、内容或 SaaS 业务中,识别“曾经极其活跃但近期流失”的高价值用户(通常称为“高价值流失预警用户”)是非常经典的业务场景。这类用户曾有良好的持续消费习惯,但因某种原因近期完全静默。找出这部分用户占比,可以帮助运营团队制定精准的召回策略。
2. 面试题信息
题目要求
找出曾经连续 3 个月、每个月都至少消费过一次,但在最近 30 天内没有任何消费和登录记录的用户,占全量注册用户的比例。
备注:假设当前系统时间为
2023-11-01(最近 30 天指2023-10-02至2023-11-01之间,包含边界)。
3. 基础数据准备
表 1:用户信息表 (user_info)
记录平台所有注册用户的信息。
| user_id | reg_date |
|---|---|
| 101 | 2023-01-01 |
| 102 | 2023-02-15 |
| 103 | 2023-03-10 |
| 104 | 2023-10-01 |
| 105 | 2023-04-20 |
表 2:用户消费订单表 (user_orders)
记录用户的消费流水。
| order_id | user_id | order_date | amount |
|---|---|---|---|
| o001 | 101 | 2023-05-10 | 100 |
| o002 | 101 | 2023-06-15 | 150 |
| o003 | 101 | 2023-07-20 | 200 |
| o004 | 102 | 2023-05-12 | 50 |
| o005 | 102 | 2023-06-18 | 80 |
| o006 | 102 | 2023-07-22 | 120 |
| o007 | 102 | 2023-10-20 | 90 |
| o008 | 103 | 2023-05-01 | 110 |
| o009 | 103 | 2023-07-05 | 130 |
| o010 | 103 | 2023-08-10 | 140 |
| o011 | 105 | 2023-06-12 | 70 |
| o012 | 105 | 2023-07-15 | 85 |
表 3:用户登录日志表 (user_logins)
记录用户的登录历史。
| login_id | user_id | login_date |
|---|---|---|
| l001 | 101 | 2023-05-10 |
| l002 | 101 | 2023-06-15 |
| l003 | 101 | 2023-07-20 |
| l004 | 101 | 2023-08-01 |
| l005 | 102 | 2023-10-20 |
| l006 | 103 | 2023-08-10 |
| l007 | 104 | 2023-10-15 |
| l008 | 105 | 2023-07-15 |
4. 期待输出结果
根据上述样例数据,分析如下:
- 用户 101:在 5、6、7 月连续消费,最近 30 天(
10-02之后)无登录、无消费。满足条件。 - 用户 102:在 5、6、7 月连续消费,但最近 30 天内有消费(
10-20)。不满足。 - 用户 103:有 5、7、8 月消费,不满足连续 3 个月。不满足。
- 用户 104:没有连续 3 个月消费记录。不满足。
- 用户 105:只有 6、7 两个月消费。不满足。
全量注册用户共 5 人,目标用户 1 人(101),占比为 20.00%。
| target_user_ratio |
|---|
| 20.00% |
5. SQL 解决方案 (基于主流标准 SQL / MySQL 8.0+)
sql
WITH user_monthly_purchase AS (
-- Step 1: 提取每个用户有消费的月份,并去重(每月保留一条记录,统一格式化为当月1号)
SELECT DISTINCT
user_id,
DATE_FORMAT(order_date, '%Y-%m-01') AS purchase_month
FROM user_orders
),
consecutive_check AS (
-- Step 2: 使用窗口函数 LEAD 获取当前月份往后的第 1 个月和第 2 个月的消费记录
SELECT
user_id,
purchase_month,
LEAD(purchase_month, 1) OVER (PARTITION BY user_id ORDER BY purchase_month) AS next_month,
LEAD(purchase_month, 2) OVER (PARTITION BY user_id ORDER BY purchase_month) AS next_next_month
FROM user_monthly_purchase
),
loyal_users AS (
-- Step 3: 筛选出曾经连续 3 个月消费的用户
SELECT DISTINCT user_id
FROM consecutive_check
WHERE DATE_ADD(CAST(purchase_month AS DATE), INTERVAL 1 MONTH) = CAST(next_month AS DATE)
AND DATE_ADD(CAST(purchase_month AS DATE), INTERVAL 2 MONTH) = CAST(next_next_month AS DATE)
),
active_users_30d AS (
-- Step 4: 找出最近 30 天内(2023-10-02 至 2023-11-01)有消费或有登录的用户
SELECT DISTINCT user_id
FROM user_orders
WHERE order_date >= DATE_SUB('2023-11-01', INTERVAL 30 DAY)
UNION
SELECT DISTINCT user_id
FROM user_logins
WHERE login_date >= DATE_SUB('2023-11-01', INTERVAL 30 DAY)
)
-- Step 5: 计算目标用户占全量用户的比例
SELECT
CONCAT(
ROUND(
COUNT(DISTINCT CASE WHEN lu.user_id IS NOT NULL AND au.user_id IS NULL THEN lu.user_id END)
/ COUNT(DISTINCT ui.user_id) * 100,
2
),
'%'
) AS target_user_ratio
FROM user_info ui
LEFT JOIN loyal_users lu ON ui.user_id = lu.user_id
LEFT JOIN active_users_30d au ON ui.user_id = au.user_id;
6. 面试深度剖析与核心考点
面试官提出这道题,主要考察候选人在以下几个维度的 SQL 实战能力和业务敏感度:
1. 连续区间的判定(最核心考点)
- 难点:如何判断“连续 3 个月”。
- 应对策略:
- 本题采用的是
LEAD()偏移窗口函数法。先将消费日期归一化为月份(当月1号),然后用LEAD(col, 1)和LEAD(col, 2)拿到当前月往后的后两次消费月。如果“当前月 + 1个月 = 下个月”且“当前月 + 2个月 = 下下个月”,则说明连续 3 个月消费。这种写法直观且在限定跨度较小(如连续3天/3个月)时性能最好。 - 替代方案(双重 Rank 差值法):如果题目变成“连续 个月”,则应使用
DENSE_RANK() OVER(...)产生连续序号,再用“月份”减去“序号”。若差值相同,则代表是连续月份。面试中可主动向面试官提及此通用方案,展示知识储备的广度。
- 本题采用的是
2. 多维度“排除”逻辑(双表联立过滤)
- 难点:最近 30 天内“既没有消费,也没有登录”。
- 应对策略:
- 通过
UNION将user_orders和user_logins中近 30 天活跃的用户取并集,生成“活跃用户白名单” (active_users_30d)。 - 在最后一步计算时,使用
LEFT JOIN配合WHERE au.user_id IS NULL(或NOT IN/NOT EXISTS)来排除这些活跃用户。
- 通过
3. 比例计算的严谨性
- 难点:分母的选择以及分子空值的处理。
- 应对策略:
- 分母必须是全量注册用户表
user_info。如果直接用user_orders算分母,会漏掉“从未消费过、只注册/登录过”的静默用户,导致占比虚高。 - 分子使用
CASE WHEN结合COUNT(DISTINCT ...)进行条件计数,能优雅地在一行 SQL 里完成过滤和比例转换,避免多次嵌套。
- 分母必须是全量注册用户表