基于本文回答
0
评论

找出曾经连续 3 个月、每个月都至少消费过一次,但在最近 30 天内没有任何消费和登录记录的用户占比

大厂 SQL 面试真题:流失的高价值用户占比分析

1. 题目背景

在电商、内容或 SaaS 业务中,识别“曾经极其活跃但近期流失”的高价值用户(通常称为“高价值流失预警用户”)是非常经典的业务场景。这类用户曾有良好的持续消费习惯,但因某种原因近期完全静默。找出这部分用户占比,可以帮助运营团队制定精准的召回策略。


2. 面试题信息

题目要求

找出曾经连续 3 个月、每个月都至少消费过一次,但在最近 30 天内没有任何消费和登录记录的用户,占全量注册用户的比例。

备注:假设当前系统时间为 2023-11-01(最近 30 天指 2023-10-022023-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 差值法):如果题目变成“连续 NN 个月”,则应使用 DENSE_RANK() OVER(...) 产生连续序号,再用“月份”减去“序号”。若差值相同,则代表是连续月份。面试中可主动向面试官提及此通用方案,展示知识储备的广度。

2. 多维度“排除”逻辑(双表联立过滤)

  • 难点:最近 30 天内“既没有消费,也没有登录”。
  • 应对策略
    • 通过 UNIONuser_ordersuser_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 里完成过滤和比例转换,避免多次嵌套。
右滑查看面试常问