横扫SQL面试——用户留存率问题

横扫SQL面试

📌 用户留存率问题

在这里插入图片描述


📝 题目题干

某APP的用户登录明细表 login_detail 记录了用户的登录行为,需要计算以下两类指标:

📊 数据表结构

login_detail 用户登录表

字段名类型说明
user_idBIGINT用户ID(唯一标识)
event_timeTIMESTAMP登录时间(精确到秒)
  1. 标准留存率🌟:用户在首次登录后第N天(精确日期)再次登录的比例
  2. 时间段活跃率🌟:用户在首次登录后的N天内(任意一天)活跃的比例

编写SQL查询,返回以下指标(保留2位小数):💻🔍💻🔍

  • 次日留存率(1_day_retention_rate)
  • 3日留存率(3_day_retention_rate)
  • 7日留存率(7_day_retention_rate)
  • 3天窗口期活跃率(3_day_activity_rate)
  • 7天窗口期活跃率(7_day_activity_rate)

精准定位问题时间点(例如用户在第3天流失率高),指导针对性改进(如第2天推送优惠券)。


user_idevent_time
10012023-08-01 09:30:15
10012023-08-02 10:15:00
10022023-08-01 14:20:30
10032023-08-01 18:45:00
10022023-08-03 11:00:00
10032023-08-05 19:30:00
  • 用户1001
    首次登录日期=2023-08-01
    次日留存检查:2023-08-02 ✅(存在记录)
    3日窗口活跃检查:2023-08-02~2023-08-04 ✅(有8月2日记录)

  • 用户1003
    首次登录日期=2023-08-01
    7日留存检查:2023-08-08 ❌(无记录)
    7天窗口活跃检查:2023-08-02~2023-08-08 ✅(有8月5日记录)


🎯 核心思路

一:标准留存率:首次登录后第N天(精确日期)

标准留存率 精准日期检查 DATE(event_time) = 首次登录 +N天 用户粘性分析、行业报告🌟🌟

📌 步骤1:计算用户首次登录日期
with first_login as (
    select 
        user_id, 
        min(date(event_time)) as first_login_date
    from login_detail
    group by user_id
)
  1. login_detail表中提取每个用户的最早登录日期 🍕. 🔍. 🔍
  2. 使用MIN(date(event_time))获取首次登录的日期(忽略时间部分)
  3. user_id分组确保每个用户仅一条记录

得到的中间表 first_login~🚀🚀🚀

user_idfirst_login_date
10012023-08-01
10022023-08-01
10032023-08-01

📌 步骤2:验证用户留存状态

retention_check as (
    select 
        -- 从 first_login 中选取用户 ID
        fl.user_id,
        -- 检查次日留存情况
        -- 使用 CASE WHEN EXISTS 语句来判断是否存在满足条件的记录
        -- 如果用户在首次登录日期的次日有登录记录,则标记为 1,表示留存
        -- 否则标记为 0,表示未留存
        case when exists (
            -- 子查询,用于检查是否存在满足条件的记录
            select 1 
            -- 从用户登录明细表 login_detail 中查询数据
            from login_detail ld 
            -- 确保子查询中的用户 ID 与 first_login 中的用户 ID 一致
            where ld.user_id = fl.user_id 
            -- 确保登录日期为首次登录日期的次日
            and date(ld.event_time) = fl.first_login_date + interval 1 day
        ) then 1 else 0 end as retained_1,
        ... -- 类似逻辑计算retained_3/retained_7

    from first_login fl
)    

处理过程

  1. 遍历first_login中每个用户
  2. 对每个用户执行三次子查询:
    • 次日留存:检查是否存在first_login_date + 1天的登录 ✅
    • 3日留存:检查是否存在first_login_date + 3天的登录 ✅
    • 7日留存:检查是否存在first_login_date + 7天的登录 ✅
  3. 存在则标记1,否则标记0

在这里插入图片描述

中间表 retention_check

user_idretained_1retained_3retained_7
1001100
1002010
1003001
  • 用户1001:次日在2023-08-02登录 ✅
  • 用户1002:第3天在2023-08-04登录 ✅
  • 用户1003:第7天在2023-08-08登录 ✅

📌 步骤3:计算留存率
用户旅程示意图:
首次登录日       次日         第3天         第7天
│             │            │            │
├─────────────┼────────────┼────────────┤
2023-08-01    2023-08-02   2023-08-04   2023-08-08
(用户1001)     ✅           ❌           ❌
(用户1002)     ❌           ✅           ❌
(用户1003)     ❌           ❌           ✅
select 
    round(avg(retained_1)*100, 2) as retention_1_day_rate,
    round(avg(retained_3)*100, 2) as retention_3_day_rate,
    round(avg(retained_7)*100, 2) as retention_7_day_rate
from retention_check;

处理过程

  1. retention_check表的标记列取平均值
    • avg(retained_1) = (1+0+0)/3 = 0.3333
    • 转化为百分比:0.3333 × 100 = 33.33%
  2. 同理计算其他留存率

最终结果表

retention_1_day_rateretention_3_day_rateretention_7_day_rate
33.3333.3333.33
-- 核心逻辑:精确匹配首次登录+N天的日期
WHERE date(ld.event_time) = fl.first_login_date + interval N day

Tips:Avg函数求比率🤣 🤣 🤣 🤣📚 给新手的AVG函数计算比率小课堂

💡 核心原理✅

AVG( [1,0,1,1,0] ) = (1+0+1+1+0)/5 = 3/5 = 0.6 → 60%

🌰 举个栗子
假设有3个用户:

| user_id | retained_1 |
|---------|------------|
| A       | 1          |
| B       | 0          |
| C       | 1          |

计算过程:

(1+0+1)/3 = 0.6666...
0.6666 × 100 = 66.6666...
ROUND后 → 66.67%

二:时间段活跃率首次登录后的N天内(任意一天)

时间段活跃率 时间段检查(如3天内) BETWEEN 首次登录+1天 AND +N天 短期行为分析、运营活动效果验证🌟🌟

场景类型典型案例
运营活动效果验证🧩双11促销期的3天转化跟踪
新手引导期监测 🧩注册后7天功能使用率统计
短期行为模式分析🧩用户领券后3天核销率追踪

📚 时间段活跃率 = 在首次登录后N天内至少活跃一次的用户比例
(如7天窗口期活跃率 = 首次后7天内任意一天登录过的用户数 / 总用户数)

📌步骤1:获取首次登录日期(同方案一)
with first_login as (
    select 
        user_id,
        min(date(event_time)) as first_login_date
    from login_detail
    group by user_id
)

在这里插入图片描述

📌步骤2:窗口期活跃验证
activity_check as (
    select 
        fl.user_id,
        -- 次日单日活跃检查
        case when exists (
            select 1 
            from login_detail ld 
            where ld.user_id = fl.user_id 
            and date(ld.event_time) = fl.first_login_date + 1
        ) then 1 else 0 end as active_1,
        
        -- 3天窗口期活跃检查
        case when exists (
            select 1 
            from login_detail ld 
            where ld.user_id = fl.user_id 
            and date(ld.event_time) between fl.first_login_date + 1 
                                      and fl.first_login_date + 3
        ) then 1 else 0 end as active_3_window,
        -- 同理:
        -- 7天窗口期活跃检查
       
    from first_login fl
)

中间表 activity_check

user_idactive_1active_3_windowactive_7_window
1001110
1002011
1003001
  • 用户1001:
    ✅ 次日活跃(8月2日)
    ✅ 3天窗口期(8月2-4日)有登录
    ❌ 7天窗口期(8月2-8日)无后续登录

📌步骤3:计算窗口期活跃率
select
    round(avg(active_1)*100, 2) as active_1_day_rate,
    round(avg(active_3_window)*100, 2) as active_3_day_window_rate,
    round(avg(active_7_window)*100, 2) as active_7_day_window_rate 
from activity_check;

最终结果表

active_1_day_rateactive_3_day_window_rateactive_7_day_window_rate
33.3366.6766.67

Tips:🎈🎈🎈

-- 包含首日后第1天到第N天(闭区间)
BETWEEN first_day+1 AND first_day+N

-- 等效写法(跨数据库兼容)
date(ld.event_time) >= first_day+1 
AND date(ld.event_time) <= first_day+N

🚨 常见误区

误区: 将窗口期误算为N个自然日
正解: 实际计算的是首日后的连续N天

-- 错误示范(可能跨月错误)
between first_day and first_day + interval '3 days'

-- 正确写法(首日+1到首日+N)
between first_day + 1 and first_day + N

场景适用逻辑核心SQL片段适用场景
标准留存率精准日期检查DATE(event_time) = 首次登录 +N天用户粘性分析、行业报告
时间段活跃率时间段检查(如3天内)BETWEEN 首次登录+1天 AND +N天短期行为分析、运营活动效果验证

给大家留一个作业~🤣 🤣 🤣 🤣

复购用户人数🚀🚀🚀

已知 order 表,表中记录了订单的相关信息,order_id 是唯一的。请通过 SQL 语句统计在 2024 年 1 月 1 日到 2024 年 3 月 10 日期间,有 30 日复购行为的人数。

表名字段名数据类型说明
orderorder_id未知(唯一标识)订单的唯一编号
orderuser_id未知用户的编号
orderpay_day字符串(格式为 YYYYMMDD)订单的支付日期

其中,30 日复购的定义为:用户在 2024 年 1 月 1 日到 2024 年 3 月 10 日这个范围内首次支付后的 30 天内又再次进行了支付。

order_iduser_idpay_day
10001user_0120240101
10002user_0220240105
10003user_0320240110
10004user_0120240115
10005user_0420240120
10006user_0220240201
10007user_0520240208
10008user_0320240215
10009user_0620240220
10010user_0120240301
10011user_0720240305
10012user_0420240310

复购用户人数SQL题解

📝 题目题干
表名字段名数据类型说明
orderorder_id唯一标识订单的唯一编号
orderuser_id字符串或数值用户的编号
orderpay_day字符串(格式为 YYYYMMDD订单的支付日期

题目要求
统计在 2024年1月1日到2024年3月10日 期间,有 30日复购行为 的用户人数。
复购定义:用户在首次支付后的30天内(含当天)再次支付。


📌步骤1:获取首次支付日期
WITH FirstOrder AS (
    SELECT 
        user_id,
        MIN(STR_TO_DATE(pay_day, '%Y%m%d')) AS first_pay_day
    FROM `order`
    WHERE 
        STR_TO_DATE(pay_day, '%Y%m%d') BETWEEN '2024-01-01' AND '2024-03-10'
    GROUP BY user_id
)
  • 使用 MIN(STR_TO_DATE(pay_day, '%Y%m%d')) 获取用户在时间段内的 首次支付日期
  • 筛选条件:仅统计首次支付在 2024-01-01 至 2024-03-10 的用户。

中间表示例

user_idfirst_pay_day
user_012024-01-01
user_022024-01-05

📌步骤2:筛选复购订单
Reorder AS (
    SELECT 
        o.user_id,
        STR_TO_DATE(o.pay_day, '%Y%m%d') AS reorder_day
    FROM `order` o
    JOIN FirstOrder fo 
        ON o.user_id = fo.user_id
    WHERE 
        -- 订单日期在首次支付后的30天内(含当天)
        STR_TO_DATE(o.pay_day, '%Y%m%d') BETWEEN fo.first_pay_day AND DATE_ADD(fo.first_pay_day, INTERVAL 30 DAY)
        -- 排除首次支付当天的订单(仅保留复购订单)
        AND STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day
)
  • 通过 BETWEEN 筛选首次支付后 30天内 的订单。
  • STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day 排除首次支付当天的订单,确保复购是 再次支付

中间表示例

user_idreorder_day
user_012024-01-15
user_022024-02-01

📌步骤3:统计复购用户数
SELECT COUNT(DISTINCT user_id) AS repurchase_count
FROM Reorder;

最终结果

repurchase_count
3

pay_day 为字符串时,需用 STR_TO_DATE(pay_day, '%Y%m%d') 显式转换。


📌 最终答案
WITH FirstOrder AS (
    SELECT 
        user_id,
        MIN(STR_TO_DATE(pay_day, '%Y%m%d')) AS first_pay_day
    FROM `order`
    WHERE 
        STR_TO_DATE(pay_day, '%Y%m%d') BETWEEN '2024-01-01' AND '2024-03-10'
    GROUP BY user_id
),
Reorder AS (
    SELECT 
        o.user_id
    FROM `order` o
    JOIN FirstOrder fo 
        ON o.user_id = fo.user_id
    WHERE 
        STR_TO_DATE(o.pay_day, '%Y%m%d') BETWEEN fo.first_pay_day 
            AND DATE_ADD(fo.first_pay_day, INTERVAL 30 DAY)
        AND STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day
)
SELECT COUNT(DISTINCT user_id) AS repurchase_count
FROM Reorder;

Tips:如果题目允许首次当天后续订单算复购

-- 修改条件为 >= 
AND STR_TO_DATE(o.pay_day, '%Y%m%d') >= fo.first_pay_day

-- 但需要去重首次订单本身:
AND o.order_id != (
    SELECT MIN(order_id) 
    FROM `order` 
    WHERE user_id = o.user_id 
      AND STR_TO_DATE(pay_day, '%Y%m%d') = fo.first_pay_day
)
过滤条件双重保险:
┌──────────────────────────────┐
│         30天时间窗口          │
│  [first_day ~ first_day+30]  │
└───────────────────┬──────────┘
                    ▼
          排除首日当天的订单
          (只保留右侧箭头部分)

整理不易 后续还会继续更新 希望列位多多支持~🚀🚀🚀

### 如何用SQL计算周留存率 为了计算周留存率,可以通过扩展上述提到的日留存逻辑来实现。以下是详细的解释以及相应的 SQL 示例。 #### 数据准备 假设存在一张 `user_log` 表,其中包含以下字段: - `user_id`: 用户唯一标识符。 - `login_time`: 登录时间戳或日期。 目标是计算一周之后仍保持活跃的用户比例。这需要将当前周期内的用户与未来第七天再次登录的用户进行匹配。 --- #### 计算周留存率的方法 核心思想是对同一张表执行 **自关联** 操作,并利用日期差函数(如 `DATEDIFF()` 或 `DATE_ADD()`)筛选符合条件的记录: 1. 将原始数据按照 `(user_id, login_time)` 去重,确保每条记录代表一次独立的用户活动。 2. 对去重后的表格进行自关联操作,设置连接条件如下: - `user_id` 相同:表示同一个用户的行为。 - 时间间隔为7天:通过比较两个 `login_time` 字段的时间差是否等于7天完成过滤。 3. 统计满足条件的用户数量并除以初始周期内的总用户数即可得到周留存率。 下面是具体的 SQL 实现代码: ```sql SELECT COUNT(DISTINCT q2.user_id) * 1.0 / COUNT(DISTINCT q1.user_id) AS weekly_retention_rate FROM ( SELECT DISTINCT user_id, DATE(login_time) AS login_date FROM user_log ) AS q1 LEFT JOIN ( SELECT DISTINCT user_id, DATE(login_time) AS login_date FROM user_log ) AS q2 ON q1.user_id = q2.user_id AND DATEDIFF(q2.login_date, q1.login_date) = 7; ``` 在此查询中, - 子查询 `q1` 提取了首次登录的用户及其对应的日期; - 子查询 `q2` 查找这些用户在未来第7天是否有新的登录记录; - 最终结果通过对两组用户的交集大小与起始用户总数之比得出每周留存比率[^4]。 --- #### 注意事项 当实际应用时需要注意一些潜在问题: - 如果数据库中的时间存储格式不是标准日期型,则需先转换成适当的形式再参与运算[^3]。 - 考虑到可能存在多时段重复访问的情况,在初步清理阶段应去除冗余项以免干扰最终统计准确性[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喻师傅

谢谢您!我会继续努力创作!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值