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

横扫SQL面试

📌 用户留存率问题


📝 题目题干

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

📊 数据表结构

login_detail 用户登录表

字段名 类型 说明
user_id BIGINT 用户ID(唯一标识)
event_time TIMESTAMP 登录时间(精确到秒)
  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_id event_time
1001 2023-08-01 09:30:15
1001 2023-08-02 10:15:00
1002 2023-08-01 14:20:30
1003 2023-08-01 18:45:00
1002 2023-08-03 11:00:00
1003 2023-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:计算用户首次登录日期
sql 复制代码
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_id first_login_date
1001 2023-08-01
1002 2023-08-01
1003 2023-08-01

📌 步骤2:验证用户留存状态
sql 复制代码
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_id retained_1 retained_3 retained_7
1001 1 0 0
1002 0 1 0
1003 0 0 1
  • 用户1001:次日在2023-08-02登录 ✅
  • 用户1002:第3天在2023-08-04登录 ✅
  • 用户1003:第7天在2023-08-08登录 ✅

📌 步骤3:计算留存率
sql 复制代码
用户旅程示意图:
首次登录日       次日         第3天         第7天
│             │            │            │
├─────────────┼────────────┼────────────┤
2023-08-01    2023-08-02   2023-08-04   2023-08-08
(用户1001)     ✅           ❌           ❌
(用户1002)     ❌           ✅           ❌
(用户1003)     ❌           ❌           ✅
sql 复制代码
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_rate retention_3_day_rate retention_7_day_rate
33.33 33.33 33.33
sql 复制代码
-- 核心逻辑:精确匹配首次登录+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个用户:

sql 复制代码
| 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:获取首次登录日期(同方案一)
sql 复制代码
with first_login as (
    select 
        user_id,
        min(date(event_time)) as first_login_date
    from login_detail
    group by user_id
)
📌步骤2:窗口期活跃验证
sql 复制代码
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_id active_1 active_3_window active_7_window
1001 1 1 0
1002 0 1 1
1003 0 0 1
  • 用户1001:
    ✅ 次日活跃(8月2日)
    ✅ 3天窗口期(8月2-4日)有登录
    ❌ 7天窗口期(8月2-8日)无后续登录

📌步骤3:计算窗口期活跃率
sql 复制代码
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_rate active_3_day_window_rate active_7_day_window_rate
33.33 66.67 66.67

Tips:🎈🎈🎈

sql 复制代码
-- 包含首日后第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天

sql 复制代码
-- 错误示范(可能跨月错误)
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 日复购行为的人数。

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

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

order_id user_id pay_day
10001 user_01 20240101
10002 user_02 20240105
10003 user_03 20240110
10004 user_01 20240115
10005 user_04 20240120
10006 user_02 20240201
10007 user_05 20240208
10008 user_03 20240215
10009 user_06 20240220
10010 user_01 20240301
10011 user_07 20240305
10012 user_04 20240310

复购用户人数SQL题解

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

题目要求

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


📌步骤1:获取首次支付日期
sql 复制代码
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_id first_pay_day
user_01 2024-01-01
user_02 2024-01-05

📌步骤2:筛选复购订单
sql 复制代码
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_id reorder_day
user_01 2024-01-15
user_02 2024-02-01

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

最终结果

repurchase_count
3

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


📌 最终答案
sql 复制代码
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:如果题目允许首次当天后续订单算复购

sql 复制代码
-- 修改条件为 >= 
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
)
sql 复制代码
过滤条件双重保险:
┌──────────────────────────────┐
│         30天时间窗口          │
│  [first_day ~ first_day+30]  │
└───────────────────┬──────────┘
                    ▼
          排除首日当天的订单
          (只保留右侧箭头部分)

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

相关推荐
Arbori_262154 分钟前
oracle常用sql
数据库·sql·oracle
SuperherRo8 分钟前
Web开发-JavaEE应用&ORM框架&SQL预编译&JDBC&MyBatis&Hibernate&Maven
前端·sql·java-ee·maven·mybatis·jdbc·hibernate
EQ-雪梨蛋花汤39 分钟前
【工具】在 Visual Studio 中使用 Dotfuscator 对“C# 类库(DLL)或应用程序(EXE)”进行混淆
数据库·ide·visual studio
阿ฅ( ̳• ε • ̳)ฅ1 小时前
C#窗体应用程序连接数据库
开发语言·数据库·c#
光军oi3 小时前
Mysql从入门到精通day5————子查询精讲
android·数据库·mysql
qr9j422335 小时前
Django自带的Admin后台中如何获取当前登录用户
数据库·django·sqlite
cherry52305 小时前
【PostgreSQL】【第4章】PostgreSQL的事务
数据库·postgresql
IT成长日记8 小时前
【MySQL基础】聚合函数从基础使用到高级分组过滤
数据库·mysql·聚合函数
Guarding and trust10 小时前
python系统之综合案例:用python打造智能诗词生成助手
服务器·数据库·python