SQL29 计算用户的平均次日留存率

困难 通过率:48.58% 时间限制:1秒 空间限制:256M

描述

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

示例:question_practice_detail

id device_id quest_id result date
1 2138 111 wrong 2021-05-03
2 3214 112 wrong 2021-05-09
3 3214 113 wrong 2021-06-15
4 6543 111 right 2021-08-13
5 2315 115 right 2021-08-13
6 2315 116 right 2021-08-14
7 2315 117 wrong 2021-08-15
……

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

avg_ret
0.3000

示例1

输入:

drop table if exists `user_profile`;
drop table if exists `question_practice_detail`;
drop table if exists `question_detail`;
CREATE TABLE `user_profile` (
`id` int NOT NULL,
`device_id` int NOT NULL,
`gender` varchar(14) NOT NULL,
`age` int ,
`university` varchar(32) NOT NULL,
`gpa` float,
`active_days_within_30` int ,
`question_cnt` int ,
`answer_cnt` int
);
CREATE TABLE `question_practice_detail` (
`id` int NOT NULL,
`device_id` int NOT NULL,
`question_id`int NOT NULL,
`result` varchar(32) NOT NULL,
`date` date NOT NULL
);
CREATE TABLE `question_detail` (
`id` int NOT NULL,
`question_id`int NOT NULL,
`difficult_level` varchar(32) NOT NULL
); INSERT INTO user_profile VALUES(1,2138,'male',21,'北京大学',3.4,7,2,12);
INSERT INTO user_profile VALUES(2,3214,'male',null,'复旦大学',4.0,15,5,25);
INSERT INTO user_profile VALUES(3,6543,'female',20,'北京大学',3.2,12,3,30);
INSERT INTO user_profile VALUES(4,2315,'female',23,'浙江大学',3.6,5,1,2);
INSERT INTO user_profile VALUES(5,5432,'male',25,'山东大学',3.8,20,15,70);
INSERT INTO user_profile VALUES(6,2131,'male',28,'山东大学',3.3,15,7,13);
INSERT INTO user_profile VALUES(7,4321,'male',28,'复旦大学',3.6,9,6,52);
INSERT INTO question_practice_detail VALUES(1,2138,111,'wrong','2021-05-03');
INSERT INTO question_practice_detail VALUES(2,3214,112,'wrong','2021-05-09');
INSERT INTO question_practice_detail VALUES(3,3214,113,'wrong','2021-06-15');
INSERT INTO question_practice_detail VALUES(4,6543,111,'right','2021-08-13');
INSERT INTO question_practice_detail VALUES(5,2315,115,'right','2021-08-13');
INSERT INTO question_practice_detail VALUES(6,2315,116,'right','2021-08-14');
INSERT INTO question_practice_detail VALUES(7,2315,117,'wrong','2021-08-15');
INSERT INTO question_practice_detail VALUES(8,3214,112,'wrong','2021-05-09');
INSERT INTO question_practice_detail VALUES(9,3214,113,'wrong','2021-08-15');
INSERT INTO question_practice_detail VALUES(10,6543,111,'right','2021-08-13');
INSERT INTO question_practice_detail VALUES(11,2315,115,'right','2021-08-13');
INSERT INTO question_practice_detail VALUES(12,2315,116,'right','2021-08-14');
INSERT INTO question_practice_detail VALUES(13,2315,117,'wrong','2021-08-15');
INSERT INTO question_practice_detail VALUES(14,3214,112,'wrong','2021-08-16');
INSERT INTO question_practice_detail VALUES(15,3214,113,'wrong','2021-08-18');
INSERT INTO question_practice_detail VALUES(16,6543,111,'right','2021-08-13');
INSERT INTO question_detail VALUES(1,111,'hard');
INSERT INTO question_detail VALUES(2,112,'medium');
INSERT INTO question_detail VALUES(3,113,'easy');
INSERT INTO question_detail VALUES(4,115,'easy');
INSERT INTO question_detail VALUES(5,116,'medium');
INSERT INTO question_detail VALUES(6,117,'easy');

复制

输出:

0.3000

解析:

  对于这道题,对于SQL能力很弱的我来说根本无从下手,实际上我在大学期间的数据库学的也不怎么样,而对于这道题中使用的一些技巧我在大学期间根本没有学,因此在这道题的解析中我会以知识点解析很详细的初学者模式学习这道题,而非解析这道题。

  牛客网官方的解题方式使用了两种解法:

  解法一,表里的数据可以看作是全部第一天来刷题了的,那么我们需要构造出第二天来了的字段,因此可以考虑用left join把第二天来了的拼起来,限定第二天来了的可以用date_add(date1,interval 1 day)=date2筛选,并用device_id限定是同一个用户。

  关于解法一,我们是先将表里的每一条数据都独立的来看,将它们都看成是第一天来刷题的,即使是有同一个人连续刷题刷了好几天,我们也将这些连续的数据独立分开来看,将每一条信息都分别看成第一天来刷题的,然后我们再进行一个合表,而合表条件我们使用date_add(date1,interval 1 day)=date2来进行匹配,这个是什么意思呢?实际上就是让date1加一天,然后看和date2是否相等,这个操作实际上就是让原数据中的日期加一天,然后让加过的表和之前的刷题信息表进行一次匹配,看看有多少能匹配上的,因为原来的信息表中保留的是原始的时间信息,而新表中的时间信息都加了一天,这样一匹配的话,有些信息的日期就在原信息中不存在了,这是因为其第二天没有刷题,因此没有这第二天的系信息,同时我们还要使用device_id限定是同一个用户,这样一来就能确保是每个人是否在第二天刷题了,这个思路实际上就是看一看某个人是否每条刷题记录都有第二天的记录。

  SQL脚本的书写方式如下:

select count(date2) / count(date1) as avg_ret
from(
select
distinct qpd.device_id,
qpd.date as date1,
uniq_id_date.date as date2
from question_practice_detail as qpd
left join(
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_date

  关于这个脚本,使用了分步查询,其中最大的外层查询,是在查询内层结果中的date2的数量与date1的数量的比值,内层查询则是对一个同样的表(这里也用了一个简单的分步,旨在去掉多余没用的数据)进行了自身的左连接,这里我们如果想要对一个同样的表进行自身的连接并且使用自身的字段时,可以加上别名加以区分,而连接的条件就是其中一个表的日期加一后和另一个表的相等,且设备id仍然相等,这样就得到了一个用户,连续两天刷题的匹配结果,而需要注意的是,左连接会保留左侧表和右侧表没有匹配的信息,因此我们使用date1的数量就可以获取到原表中所有的刷题信息,而右侧匹配到的就是有第二天刷题的信息,这样做比值,就可以得到我们想要的结果了。

  在这个问题中其实非常重要的一个问题就是:什么是用户的平均次日存留率?这个怎么计算是个问题,在题目中解释道:用户在某天刷题后第二天还会再来刷题的平均概率,这个是什么意思呢?我们在解决问题的时候一定要学会理解问题并简化问题,在某天刷题之后第二天还会来,其意思就是,针对于用户的每一条记录,在第二天还有这个人的刷题记录,并且这个第二天是一个相对的第二天,是一个相对概念,也就是说如果我连着刷题刷了三天:10号,11号,12号,那么11号相对于10号就是第二天,而12号相对于11号也是第二天,总体上这三天中存在两个第二天,这就让我们必须单独的看待每一天,然后找每一天的第二天,而不能在内心中存在12号是10号的第三天的想法,这种想法尽管首先上就是不对的,但是一些人在做题的时候内心还是隐约有这种顾虑,比如我,进而难以很好的理解这道题的意思,这道题的第二天就是指相对的第二天,我们必须要独立的来看待每一天,然后找到每一天的第二天,然后我们要求的概率,实际上就是所有的有第二天的信息所占的比例,也就是说所有有第二天记录的记录在所有记录中所占据的比例,如上边我举的例子,三天中有两天有第二天,而总共有三天,那么我第二天还来刷题的概率就是2/3,这三天之中。有两天拥有第二天,有一天不拥有,这个概率就是这样来的。

  解法二,用lead函数将同一个用户连续两天的记录拼接起来。先按用户分组partition by device_id,再按日期升序排序order by date,再两两拼接(最后一个默认和null拼接),即lead(date) over (partition by device_id order by date)

  关于解法二我基本上完全不懂,因为我不知道lead函数是什么意思,因此我查了一下:LEAD()函数是一个窗口函数,允许我们向前看多行并从当前访问行的数据,LEAD()函数对于计算同一结果集中当前行和后续行之间的差异非常有用,LEAD()函数的用法:

LEAD(<expression>[,offset[, default_value]]) OVER (
PARTITION BY (expr)
ORDER BY (expr)
)

  其中expression是LEAD()函数返回的值experssion从offset-th有序分区排,offset是从当前行向前行的行数,以获取值,offset必须是一个非负整数,如果offset为零,则LEAD()函数计算expression当前行的值,如果省略offset,则LEAD()函数默认使用一个。default_value,如果没有后续行,则LEAD()函数返回default_value。例如,如果offset是1,则最后一行的返回值为default_value。如果未指定default_value,则函数返回NULL。PARTITION BY子句将结果集中的行划分LEAD()为应用函数的分区,如果PARTITION BY未指定子句,则结果集中所有行都被视为单个分区,ORDER BY子句是确定函数中行的顺序用的。

MySQL LEAD()函数示例

/*
我们将使用示例数据库中的orders和customers表进行演示:
以下语句查找每个客户的订单日期和下一个订单日期:
*/
SELECT
customerName,/*字段*/
orderDate,/*字段*/
LEAD(orderDate,1) OVER (/*获取下一行的orderDate*/
PARTITION BY customerNumber /*按照customerName分组*/
ORDER BY orderDate /*排序*/) nextOrderDate/*命名为nextOrderDate*/
FROM
orders
INNER JOIN customers USING (customerNumber);

  我们将获得一个这样的结果:

+------------------------------------+------------+---------------+
| customerName | orderDate | nextOrderDate |
+------------------------------------+------------+---------------+
| Atelier graphique | 2013-05-20 | 2014-09-27 |
| Atelier graphique | 2014-09-27 | 2014-11-25 |
| Atelier graphique | 2014-11-25 | NULL |
| Signal Gift Stores | 2013-05-21 | 2014-08-06 |
| Signal Gift Stores | 2014-08-06 | 2014-11-29 |
| Signal Gift Stores | 2014-11-29 | NULL |
| Australian Collectors, Co. | 2013-04-29 | 2013-05-21 |
| Australian Collectors, Co. | 2013-05-21 | 2014-02-20 |
| Australian Collectors, Co. | 2014-02-20 | 2014-11-24 |
| Australian Collectors, Co. | 2014-11-24 | 2014-11-29 |
| Australian Collectors, Co. | 2014-11-29 | NULL |
| La Rochelle Gifts | 2014-07-23 | 2014-10-29 |
| La Rochelle Gifts | 2014-10-29 | 2015-02-03 |
| La Rochelle Gifts | 2015-02-03 | 2015-05-31 |
| La Rochelle Gifts | 2015-05-31 | NULL |
| Baane Mini Imports | 2013-01-29 | 2013-10-10 |
| Baane Mini Imports | 2013-10-10 | 2014-10-15 |
...
/*
在此示例中,我们首先将客户编号的结果集划分为多个分区。然后,我们按订单日期对每个分区进行排序。最后,LEAD()函数应用于每个分区以获取下一个订单日期。 一旦后续行穿过分区边界,nextOrderDate每个分区的最后一行中的值就是NULL。
*/

  现在我们知道了LEAD()的用法,实际上就是找下n行的n个字段,因此我们就可以理解这道题的解法了:先是按用户分组,然后按照时间升序排序,然后我们将连续两天的记录拼合起来,然后我们就可以检查两个字段的日期差了,只要日期差为1,那就说明这是一个连续刷题的记录,而为0的话说明这天的第二天没有连续刷题,然后日期差为1的记录/所有的记录即为我们想要求的概率,这里可以使用平均值计算出来,也就是求日期差为1的个数的平均值。

  SQL脚本:

select avg(if(datediff(date2, date1)=1, 1, 0)) as avg_ret/*差值为1则为1,否则为0,然后求这个字段相加的平均值*/
from (
select
distinct device_id,
date as date1,
lead(date) over (partition by device_id order by date) as date2/*获取相应的分组以及分组内下行的日期*/
from (
select distinct device_id, date/*简化表格*/
from question_practice_detail
) as uniq_id_date
) as id_last_next_date

SQL29 计算用户的平均次日留存率的更多相关文章

  1. 游戏数据分析中“次日留存率”与“游戏生命周期第N天上线率”的SAS实现

    在游戏行业,次日留存率是个很重要的指标,对于评价一款游戏的优劣具有很重要的参考价值. 下面先看以下相关的定义: 用户留存:统计时间区间内,新登用户在随后不同时期的登录使用情况. 日次留存率:日新登用户 ...

  2. 编写一个ComputerAverage抽象类,类中有一个抽象方法求平均分average,可以有参数。定义 Gymnastics 类和 School 类,它们都是 ComputerAverage 的子类。Gymnastics 类中计算选手的平均成绩的方法是去掉一个最低分,去掉一个最高分,然后求平均分;School 中计算平均分的方法是所有科目的分数之和除以总科目数。 要求:定义ComputerAv

    题目: 编写一个ComputerAverage抽象类,类中有一个抽象方法求平均分average,可以有参数. 定义 Gymnastics 类和 School 类,它们都是 ComputerAverag ...

  3. 小白学数据分析--留存率分析_I次日留存率突然下降了50%

    小白学数据分析--留存率分析_I次日留存率突然下降了50% 最近在做留存分析时,遇到了不少的情况,也经常会有人问我,为什么我的游戏突然次日留存率降了一半.如果留存率是单单作为一个简单的指标的话,那对你 ...

  4. 活跃天数计算用户等级模仿QQ的升级方式

    QQ等级的算法:设当前等级为N,达到当前等级最少需要的活跃天数为D,当前活跃天数为Dc,升级剩余天数为Dr,则: 从而推出: 好了,引述完成,懒得写字了,贴出代码: 复制内容到剪贴板 代码: < ...

  5. ac命令根据/var/log/wtmp文件登录退出时间计算用户连接时间

    ac命令根据/var/log/wtmp文件登录退出时间计算用户连接时间

  6. /* * 有五个学生,每个学生有3门课的成绩,从键盘输入以上数据 *(包括学生号,姓名,三门课成绩),计算出平均成绩, *将原有的数据和计算出的平均分数存放在磁盘文件"stud"中。 */

    1.Student类:类中有五个变量,分别是学号,姓名,三门成绩 package test3; public class Student { private int num; private Stri ...

  7. js用img代替ajax js心跳 向服务器定时传送参数 主要计算用户在线时长

    html: <!doctype html><html><head><meta charset="utf-8"><title&g ...

  8. MySQL学习_计算用户支付方式占比_20161104

    计算用户支付方式占比 SELECT b.*#根据城市ID 年月排序 FROM ( SELECT a.* FROM ( #纵向合并 SELECT b1.ID,a1.城市,a1.收款方式,DATE_FOR ...

  9. 代码实现:有五个学生,每个学生有3门课的成绩,从键盘输入以上数据 (包括学生号,姓名,三门课成绩),计算出平均成绩,将原有的数据和计算出的平均分数存放在磁盘文件"stud"中。

    import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.ut ...

  10. MySQL 的 RowNum 实现(排行榜计算用户排名)

    1. 计算用户排名最高效的方法 例如:通过用户分享个数排名,那么自己的排名就是:比自己分享数多的用户个数 + 1 ' and `count` > '自己分享个数' 缺点:当多个用户分享个数相同的 ...

随机推荐

  1. 2021年9月28日,老是遇到一些非常奇葩的问题。就离谱、好好的一个web项目就莫名奇妙坏了。

    起因是这样的:我前几天用idea2020编辑器,用ssm框架搭建了一个图书管理系统.只是将图书信息的增删改查实现,还有用户的注册和登录功能实现.本来想着今天将用户信息的删除和修改完善以下,本来是很简单 ...

  2. 记录一次成功反混淆脱壳及抓包激活app全过程

    记录一次成功反混淆脱壳及抓包激活app全过程 前言 ​ 近期接到一个需求,要对公司之前开发的一款app进行脱壳.因为该app是两年前开发的,源代码文件已经丢失,只有加壳后的apk文件,近期要查看其中一 ...

  3. BI系统打包Docker镜像及部署的技术难度和实现

    BI系统打包Docker镜像及部署的技术难度和实现 随着容器化技术盛行,Docker在前端领域也有着越来越广泛的应用:传统的前端部署方式需要我们将项目打包生成一系列的静态文件,然后上传到服务器,配置n ...

  4. 第2-1-4章 SpringBoot整合FastDFS文件存储服务

    目录 5 SpringBoot整合 5.1 操作步骤 5.2 项目依赖 5.3 客户端开发 5.3.1 FastDFS配置 5.3.2 FastDFS配置类 5.3.3 文件工具类 5.3.4 文件上 ...

  5. 如何通过free看懂内存的真实使用

    之前有位同事问过Linux系统内存free命令下各参数的区别与关系,自己也没太明白,有点尴尬.今天整理一下,供了解. free命令是Liunx操作系统中对内存进行查看和监控的一个常用命令.我们可以直接 ...

  6. springboot集成支付宝的支付(easy版)

    SpringBoot对接支付宝 需要先注册账号 到支付宝开发者平台创建网页支付应用 启用公钥模式 需要使用到appId和下面的两个秘钥 写配置信息的代码 1.引入依赖 <dependency&g ...

  7. K8Snode节点管理集群资源方法

    1.1 方法1 1.将master的admin.conf 文件拷贝到 node节点 [root@k8s-m ~]#scp /etc/kubernetes/admin.conf root@192.168 ...

  8. HTTPS - 揭秘 TLS 1.2 协议完整握手过程--此文为转发文,一定要结合wirshark工具看,很清楚

    winshark 筛选条件为:tls and ip.src==xxx 本文通过对一次 TLS 握手过程的数据抓包分析做为切入点,希望能进一步的帮助大家理解 HTTPS 原理. HTTPS 是建立在 S ...

  9. Go语言核心36讲17

    在前面的文章中,我们已经提到过很多次"指针"了,你应该已经比较熟悉了.不过,我们那时大多指的是指针类型及其对应的指针值,今天我们讲的则是更为深入的内容. 让我们先来复习一下. ty ...

  10. Ian Lance Taylor

    https://img.mukewang.com/5a9dfda50001933e23006728.png 在GCC的世界中,没有人比Ian更火.在GCC maillist中,Ian的身影呈现在前端中 ...