SQL题目分析之计算用户的平均次日留存率

 更新时间:2026年04月04日 08:42:53   作者:努力学代码的小two  
留存率是指在特定时间段内,仍然继续使用某项产品或服务的用户占用户总数的百分比,这篇文章主要介绍了SQL题目分析之计算用户的平均次日留存率的相关资料,需要的朋友可以参考下

描述

题目:现在运营想要查看用户在某天刷题后第二天还会再来刷题的留存率。请你取出相应数据。

示例:question_practice_detail

iddevice_idquestion_idresultdate
12138111wrong2021-05-03
23214112wrong2021-05-09
33214113wrong2021-06-15
46543111right2021-08-13
52315115right2021-08-13
62315116right2021-08-14
72315117wrong2021-08-15
83214112wrong2021-05-09
93214113wrong2021-08-15
106543111right2021-08-13
112315115right2021-08-13
122315116right2021-08-14
132315117wrong2021-08-15
143214112wrong2021-08-16
153214113wrong2021-08-18
166543111right2021-08-13

根据示例,你的查询应返回以下结果:

avg_ret
0.3000

题目分析

需求:计算用户在某天刷题后,第二天还会再来刷题的留存率

什么是次日留存率?次日留存率 = (第一天刷题的用户中,第二天也来刷题的用户数) / (第一天刷题的总用户数)

示例解读:根据示例数据,最终的平均留存率是 0.3000,即 30%。这意味着,在所有某天刷过题的用户中,平均有 30% 的用户会在第二天继续刷题。

关键点

  1. 用户标识:我们通过 device_id 来唯一标识一个用户。
  2. 行为日期:我们关心的是用户刷题的日期 (date)。
  3. 去重:一个用户在同一天可能刷了多道题,但我们只关心他 “是否来过”,所以需要对 (device_id, date) 进行去重。
  4. 关联:我们需要将用户第一天的行为和第二天的行为关联起来,以判断他是否留存。

解题思路

要计算留存率,我们需要明确两个集合:A. 某日活跃用户集合:在某天 date1 刷过题的用户。B. 次日留存用户集合:在集合 A 中的用户,并且在 date1 的第二天(date1 + 1天)也刷过题。

留存率就是 |B| / |A|

直接计算比较复杂,我们可以换个角度,为每一条 “某日刷题记录” 匹配一条 “次日刷题记录”,然后通过计数来求比率。

  1. 构建用户每日刷题的唯一记录:首先,我们需要一个只包含 (device_id, date) 唯一组合的数据集。
  2. 自连接匹配次日记录:将这个唯一记录数据集与自身进行左连接(LEFT JOIN)。连接条件是:
    • A.device_id = B.device_id (同一个用户)
    • B.date = A.date + INTERVAL 1 DAY (B 表的日期是 A 表日期的第二天)
  3. 计算留存率
    • 左连接的好处是,如果一个用户在第二天没有来,B.date 字段会是 NULL
    • 因此,COUNT(B.date) 就等于 “第二天也来的用户数”。
    • COUNT(A.date) 就等于 “第一天来的总用户数”。
    • 两者相除,就得到了我们想要的平均次日留存率。

分步解析最终代码

-- 外层查询:计算平均次日留存率
select count(date2)/count(date1) as avg_ret
from(
    -- 子查询 A: 为每个用户的每日刷题记录匹配次日是否也刷题
    select distinct 
        qpd.device_id,
        qpd.date as date1,  -- 用户当天刷题的日期
        uniq_id_date.date as date2  -- 用户次日是否刷题的日期(可能为NULL)
    from 
        question_practice_detail as qpd
    left join(
        -- 子查询 B: 获取所有用户所有刷题日期的唯一记录
        select distinct device_id, date
        from question_practice_detail
    )as uniq_id_date
    -- 左连接条件
    on qpd.device_id = uniq_id_date.device_id  -- 同一个用户
    and date_add(qpd.date, interval 1 day) = uniq_id_date.date -- 次日
)as id_last_next_code;

1. 子查询 B (uniq_id_date)

select distinct device_id, date
from question_practice_detail
  • 目标:创建一个 “用户 - 日期” 的唯一映射表。
  • 作用:这个子查询是整个逻辑的基石。它确保了我们处理的每一条记录都代表一个用户在某一天的一次独立访问行为,无论他当天刷了多少道题。
  • 结果示例(基于题目数据):
    device_iddate
    21382021-05-03
    32142021-05-09
    32142021-06-15
    65432021-08-13
    23152021-08-13
    23152021-08-14
    ......

2. 子查询 A (id_last_next_code)

select distinct 
    qpd.device_id,
    qpd.date as date1,
    uniq_id_date.date as date2
from 
    question_practice_detail as qpd
left join
    uniq_id_date
on 
    qpd.device_id = uniq_id_date.device_id
    and date_add(qpd.date, interval 1 day) = uniq_id_date.date
  • 目标:这是核心的匹配步骤。对于原始表中的每一条刷题记录(这里用 qpd 别名),我们都尝试在 uniq_id_date 表中找到该用户次日的刷题记录。
  • FROM question_practice_detail as qpd:我们从原始表开始,而不是从去重后的表开始。这是为了确保我们考虑到所有的 “刷题日”,即使一个用户一天刷了多道题,distinct 会保证最终每 (device_id, date1) 只留下一条记录。
  • LEFT JOIN uniq_id_date:使用左连接至关重要。它会保留 qpd 表中的所有记录。如果在 uniq_id_date 中找到了匹配的次日记录,date2 就会有值;如果没找到,date2 就会是 NULL
  • date_add(qpd.date, interval 1 day):这是一个日期函数,用于计算 qpd.date 的第二天。
  • select distinct ...:再次使用 distinct 是为了确保对于同一个用户在同一天的多次刷题行为,我们在最终的派生表中只保留一条 (device_id, date1, date2) 记录。
  • 结果示例
    device_iddate1date2
    21382021-05-03NULL-- 2138 在 5 月 4 日没有来
    32142021-05-09NULL-- 3214 在 5 月 10 日没有来
    23152021-08-132021-08-14-- 2315 在 8 月 14 日来了
    23152021-08-142021-08-15-- 2315 在 8 月 15 日来了
    23152021-08-15NULL-- 2315 在 8 月 16 日没有来
    .........

3. 外层查询

select count(date2)/count(date1) as avg_ret
from id_last_next_code;
  • 目标:计算最终的平均留存率。
  • count(date2):计算 id_last_next_code 表中 date2 字段不为 NULL 的行数。这正好等于 “次日留存的用户行为次数”。
  • count(date1):计算 id_last_next_code 表中 date1 字段不为 NULL 的行数。这正好等于 “第一天的总用户行为次数”。
  • count(date2)/count(date1):两者相除,得到的就是平均次日留存率。
  • 在我们的示例中,如果 count(date2) 是 3count(date1) 是 10,那么结果就是 3/10 = 0.3

总结

这道题的解法非常巧妙地运用了“自连接”“左连接”的技巧。

  1. 自连接:通过将一个去重后的 “用户 - 日期” 表与自身连接,我们能够在一条记录里同时看到一个用户在两天的行为。
  2. 左连接:通过左连接,我们确保了不会丢失任何一个 “第一天” 的用户行为记录,同时用 NULL 值清晰地标记出了哪些用户没有在 “第二天” 回来。
  3. 计数与除法:最后,利用 COUNT() 函数对非空值的计数特性,轻松地计算出了分子(留存数)和分母(总活跃数),从而得到了最终的留存率。

这种模式在用户行为分析中非常常见,例如计算 7 日留存、月留存等,核心思想都是类似的。

到此这篇关于SQL题目分析之计算用户的平均次日留存率的文章就介绍到这了,更多相关SQL计算用户平均次日留存率内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 哪些情况会导致 MySQL 索引失效

    哪些情况会导致 MySQL 索引失效

    这篇文章主要介绍了哪些情况会导致MySQL索引失效,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下
    2022-07-07
  • MySQL5.6.31 winx64.zip 安装配置教程详解

    MySQL5.6.31 winx64.zip 安装配置教程详解

    这篇文章主要介绍了MySQL5.6.31 winx64.zip 安装配置教程详解,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-02-02
  • MySQL密码正确却无法本地登录的解决方法

    MySQL密码正确却无法本地登录的解决方法

    这篇文章主要介绍了MySQL密码正确却无法本地登录的解决方法,需要的朋友可以参考下
    2014-12-12
  • 基于proxysql实现MySQL读写分离的实现实例

    基于proxysql实现MySQL读写分离的实现实例

    这篇文章主要介绍了基于proxysql实现MySQL读写分离,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-11-11
  • SQL面试题:求时间差之和(有重复不计)

    SQL面试题:求时间差之和(有重复不计)

    这篇文章主要介绍了SQL面试题:求时间差之和(有重复不计),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • MySQL5.7升级MySQL8.0的完整卸载与安装及连接Navicat的步骤

    MySQL5.7升级MySQL8.0的完整卸载与安装及连接Navicat的步骤

    因为一个项目交接需要需要将mysql物理备份文件还原至MySQL5.7,并且将mysql5.7升级到MySQL8.0,下面这篇文章主要给大家介绍了关于MySQL5.7升级MySQL8.0的完整卸载与安装及连接Navicat的相关资料,需要的朋友可以参考下
    2023-03-03
  • MySQL中的GROUP_CONCAT()函数使用及说明

    MySQL中的GROUP_CONCAT()函数使用及说明

    GROUP_CONCAT()函数在MySQL中用于将多行数据合并为一行,通过本文我们可以了解其基本语法、应用示例、参数使用以及注意事项
    2025-12-12
  • MySQL数据库索引order by排序精讲

    MySQL数据库索引order by排序精讲

    我们今天说的并不是大数据下该如何优雅的排序,如何提升排序性能的问题,我们来仔细分析说一说MySQL中的排序问题,希望可以对正在阅读的同学们有所启迪帮助
    2021-11-11
  • 超详细MySQL使用规范分享

    超详细MySQL使用规范分享

    这篇文章主要介绍了MySQL使用规范,帮助大家更规范的操作MySQL,感兴趣的朋友可以了解下
    2020-08-08
  • 通过Query Profiler查看MySQL语句运行时间的操作方法

    通过Query Profiler查看MySQL语句运行时间的操作方法

    这篇文章主要介绍了通过Query Profiler查看MySQL语句运行时间的操作方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03

最新评论