首页 > 试题广场 >

牛客每个人最近的登录日期(五)

[编程题]牛客每个人最近的登录日期(五)
  • 热度指数:144433 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
牛客每天有很多人登录,请你统计一下牛客每个日期新用户的次日留存率。
有一个登录(login)记录表,简况如下:
id
user_id client_id
date
1 2 1 2020-10-12
2 3 2 2020-10-12
3 1 2 2020-10-12
4 2 2 2020-10-13
5 1 2 2020-10-13
6 3 1 2020-10-14
7 4 1 2020-10-14
8 4 1 2020-10-15
第1行表示user_id为2的用户在2020-10-12使用了客户端id为1的设备登录了牛客网,因为是第1次登录,所以是新用户
......
第4行表示user_id为2的用户在2020-10-13使用了客户端id为2的设备登录了牛客网,因为是第2次登录,所以是老用户
......
最后1行表示user_id为4的用户在2020-10-15使用了客户端id为1的设备登录了牛客网,因为是第2次登录,所以是老用户

请你写出一个sql语句查询每个日期新用户的次日留存率,结果保留小数点后面3位数(3位之后的四舍五入),并且查询结果按照日期升序排序,上面的例子查询结果如下:
date
p
2020-10-12
0.667
2020-10-13
0.000
2020-10-14
1.000
2020-10-15
0.000
查询结果表明:
2020-10-12登录了3个(user_id为2,3,1)新用户,2020-10-13,只有2个(id为2,1)登录,故2020-10-12新用户次日留存率为2/3=0.667;
2020-10-13没有新用户登录,输出0.000;
2020-10-14登录了1个(user_id为4)新用户,2020-10-15,user_id为4的用户登录,故2020-10-14新用户次日留存率为1/1=1.000;
2020-10-15没有新用户登录,输出0.000;
(注意:sqlite里查找某一天的后一天的用法是:date(yyyy-mm-dd, '+1 day'),sqlite里1/2得到的不是0.5,得到的是0,只有1*1.0/2才会得到0.5)


示例1

输入

drop table if exists login;
CREATE TABLE `login` (
`id` int(4) NOT NULL,
`user_id` int(4) NOT NULL,
`client_id` int(4) NOT NULL,
`date` date NOT NULL,
PRIMARY KEY (`id`));

INSERT INTO login VALUES
(1,2,1,'2020-10-12'),
(2,3,2,'2020-10-12'),
(3,1,2,'2020-10-12'),
(4,2,2,'2020-10-13'),
(5,1,2,'2020-10-13'),
(6,3,1,'2020-10-14'),
(7,4,1,'2020-10-14'),
(8,4,1,'2020-10-15');

输出

2020-10-12|0.667
2020-10-13|0.000
2020-10-14|1.000
2020-10-15|0.000
select l.date,IFNULL(convert((select count(distinct l3.user_id) from login l3,(select distinct l2.user_id from login l2 where l2.date = l.date and l2.user_id 
                      not in (select distinct l1.user_id from login l1 where l1.date < l.date)) l4 
                      where l3.date = DATE_ADD(l.date,INTERVAL 1 DAY) and l4.user_id = l3.user_id)
               /(select count(distinct l2.user_id) from login l2 where l2.date = l.date and l2.user_id 
                      not in (select distinct l1.user_id from login l1 where l1.date < l.date))
                      ,decimal(10,3)),0) p from login l group by l.date order by l.date asc

直接粗暴统计
发表于 2021-03-16 14:23:44 回复(0)
更多回答
with tem01 as
(select 
date,
case when(user_id,date) in 
(select user_id,min(date) from login group by user_id )
then 1 else 0 end SUM01,
case when(user_id,date) in 
(select user_id,date_add(date,interval 1 day) from login)
then 1 else 0 end SUM02
from login),
tem02 as
(select 
date,
sum(SUM01) count01,
lead(sum(SUM02),1,0) over(order by date) count02
from tem01
group by date
order by date)
select 
date,
ifnull(round(count02/count01*1.0,3),0)
from tem02
各位大佬帮忙看看什么情况,自测运行可以,保存提交出问题了
发表于 2022-08-09 17:16:38 回复(0)

求好心人帮我看看我哪里出错了,自测运行通过了,但是只有4/5用例通过,感激
select date,round(ifnull(1_count/0_count,0),3) p
from
(select date,
count(distinct if(DATEDIFF(next_date,date)=1,user_id,null)) 1_count,
count(distinct if(date=next_date and next_date=min_date,user_id,null)) 0_count
from
(select *,min(date) over(partition by user_id) min_date
from 
(select l1.*,l2.date next_date
from login l1
join login l2 on l1.user_id=l2.user_id and l2.date>=l1.date
order by l1.user_id,l1.date) m) s
group by date
order by date) sub

发表于 2022-06-15 16:06:43 回复(0)
select date,
ifnull(
round(
sum(case when (user_id,date) in 
(select user_id,min(date) from login group by user_id) and
   (user_id,date_add(date,interval 1 day)) in 
   (select user_id,date from login) then 1 else 0 end)
/
sum(case when (user_id,date) in 
(select user_id,min(date) from login group by user_id) then 1 else 0 
    end),3),0) p
from login
group by date
order by date;
发表于 2022-04-15 21:51:45 回复(0)
每个日期新用户次日留存数
select t1.date date,
sum(case when t1.r=1 and t2.date-t1.date=1 and t1.user_id=t2.user_id then 1 else 0 end) num_keep
from
(select user_id, date, rank() over(partition by user_id order by date) r
from login) t1,
(select user_id, date, rank() over(partition by user_id order by date) r
from login) t2
group by t1.date
每个日期新用户数目
select date,sum(case when r=1 then 1 else 0 end) total_num from
(select date, rank() over(partition by user_id order by date) r
from login) t
group by date
order by date
最终解答
select t_1.date date, round(case when total_num=0 then 0 else num_keep/total_num end, 3) p
from
(select t1.date date,
sum(case when t1.r=1 and t2.date-t1.date=1 and t1.user_id=t2.user_id then 1 else 0 end) num_keep
from
(select user_id, date, rank() over(partition by user_id order by date) r
from login) t1,
(select user_id, date, rank() over(partition by user_id order by date) r
from login) t2
group by t1.date) t_1
left join 
(select date,sum(case when r=1 then 1 else 0 end) total_num from
(select date, rank() over(partition by user_id order by date) r
from login) t
group by date
order by date) t_2
on t_1.date = t_2.date
order by t_1.date




发表于 2022-01-25 10:32:40 回复(0)
select t.date,
ifnull(
    round(
        sum(case when 
            (t.user_id,t.date) in (select user_id, date_add(min(date),interval 1 day) from login group by user_id)
            then 1 else 0 end)
        /
        sum(case when (t.user_id,t.date) in (select user_id, min(date)from login group by user_id) then 1 else 0 end) 
        ,3)
    ,0)
from login t group by t.date

请教一下,个人理解上述可以通过,但是为什么计算出来比率是0呢?有大神能指导下吗?

发表于 2021-12-15 23:50:05 回复(0)
select distinct
l.date,ifnull(round(c.cc/a.dd,3),0)as p
FROM
login as l
left join 
(
    select date,count(date) as cc from login
    WHERE (user_id,date) in (select user_id,date_add(date,interval 1 day) from login group by user_id)
    group by date
) as c on date_add(l.date,interval 1 day)= c.date
left JOIN
(select date,count(l2.user_id) as dd 
 from login l2
 join
 (select user_id,min(date) f from login group by user_id) as e on e.user_id =l2.user_id and e.f=date
 group by date) as a on a.date=l.date
让你们看看我肝了90分钟的愚蠢的代码
发表于 2021-12-06 00:03:52 回复(0)
select t2.date,round(ifnull(ifnull(t1.c1,0)/t2.c2,0),3)
from(select count(user_id) c1,DATE_ADD(date,INTERVAL -1 day) date
from login
where (user_id,date) in 
(select user_id,DATE_ADD(min(date),INTERVAL 1 day) from login group by user_id)
group by date) t1

right join(select date,sum(case rk when 1 then 1 else 0 end) c2
from (select date,row_number() over(partition by user_id order by date) rk from login) t
group by date) t2
on t1.date = t2.date
发表于 2021-12-04 00:47:51 回复(1)
with t as(
select user_id, min(date) am  from login
group by user_id 
    )

select distinct f.date ,ifnull(g.gav,0.000)  from login f left join (
    select t.am g3,round(  ifnull( e.co2,0)/count(1),3 ) gav from  t inner join(
    select t1.am1 am2 ,count(1) co2 from (
            select a.user_id, t.am am1 from login a 
            inner join t on a.user_id = t.user_id and a.date= date_add( t.am , interval 1 day  )
        ) t1
        group by t1.am1
    ) e
    on e.am2 = t.am
    group by t.am 
)g
on g.g3 = f.date
写了2小时,成功写成了一坨屎

发表于 2021-11-25 15:13:29 回复(0)
我觉得这一题有问题吧,2020/10/13和2020/10/15这两天无新用户,如果设置这两天新用户次日登陆率为o的情况,即默认这两天存在新用户,且新用户次日均未登录,还是计算结果保持为none更合适一些
发表于 2021-07-15 16:41:53 回复(0)
把login中每个数据都鉴别出是否为新用户和是否为新用户且次日留存
select a.date,round(IFNULL(sum(a.liucun)/sum(a.new), 0),3) from
(
    select l.user_id,l.date,
    case when (l.user_id,l.date) in  
    (
         select user_id as u_id,date_sub(date,interval 1 DAY) as d
         from login
         where (user_id,date) in (select user_id,DATE_ADD(min(date),interval 1 DAY) from login group by user_id)
    ) then 1 else 0
    end as liucun,
    case when (l.user_id,l.date) in
    (
        select user_id as u_id,min(date) as d
        from login
        group by user_id
    ) then 1 else 0
    end as new
    from login as l
) as a
group by a.date


发表于 2021-06-19 18:05:56 回复(0)

解法一

-- (user_id, date) --> (用户id, 首次登陆日期) 
with t1 as (
    select user_id, min(date) as date from login group by user_id
),
-- 得到正序排列的所有日期
t2 as (
    select distinct date from login order by date asc
),
-- (user_id, first_login_date)
t3 as (
    select
        user_id,
        t2.date as first_login_date
        from t2 left join t1 on t2.date = t1.date
),
-- 准备查询条件: (user_id, sencond_login_date) -> (用户id,次日登陆的日期)
t4 as (
    select user_id, 
    DATE_ADD(min(date),INTERVAL 1 DAY) sencond_login_date
    from login group by user_id
),
-- (user_id, sencond_login_date) --> (用户id, 次日登陆的日期)
t5 as (
    select 
        user_id,
        date as sencond_login_date
    from login 
    where 
        (user_id, date) in (select user_id, sencond_login_date from t4)
), 
t6 as (
    select 
        t3.first_login_date,
        t3.user_id,
        t5.sencond_login_date
    from t3 left join t5 on t3.user_id = t5.user_id
)
select 
        distinct first_login_date,
        round(
             count(sencond_login_date) over(partition by first_login_date) /
             count(first_login_date) over(partition by first_login_date),
            3
        )
    from t6
	


编辑于 2021-05-27 23:53:08 回复(0)
用了with as
我解题思路就是构造两个表,按日期分的。一个是每个日期的新用户总数,一个是每个日期新用户留存数。构造完成之后按照日期链接,p=新用户留存数/新用户总数  以下:
with b as (select y.user_id,k.date from (select user_id,min(date)as date from login
group by user_id)as y right join  (select distinct date from login)as k on
y.date=k.date) 新建立了一个这样的临时表。
这个表基本上就是每个日期的新用户个数表了,后面用count就可以
select date,count(user_id)as cnt from b group by date

再构造一个每个日期新用户留存个数表
select m.date,count(n.user_id)as s from 
 (select b.user_id,b.date from b join login as l on b.user_id=l.user_id and
l.date=date_add(b.date,interval 1 day))as n right join (select distinct date from login)as m on n.date=m.date group by m.date

当时用join链接  ,觉得肯定没问题了,后来发现如果分母为0的分式根本没意义。就在刚开始的select后加了个条件,所以如果当分子是0,分母也是0的时候,要得出是0的概率,可以把分母改成1或者其实数字。所以完整的式:

with b as (select y.user_id,k.date from (select user_id,min(date)as date from login
group by user_id)as y right join  (select distinct date from login)as k on
y.date=k.date) 
select T.date,round(H.s/(case when H.s=0 then 1 else T.cnt end) ,3)as p from 
(select date,count(user_id)as cnt from b group by date)as T 
join
(select m.date,count(n.user_id)as s from 
 (select b.user_id,b.date from b join login as l on b.user_id=l.user_id and
l.date=date_add(b.date,interval 1 day))as n right join (select distinct date from login)as m on n.date=m.date group by m.date )as H on
H.date=T.date




发表于 2021-05-24 15:02:57 回复(0)
select dt,round(nvl(lx/new,0),3) from (
select dt1,
       count(1)as lx
from ( 
select 
     user_id,
     max(case when rk=1 then dt end) as dt1,
     max(case when rk=2 then dt end) as dt2
from 
(select login.*,row_number() over(partition by user_id order by dt) rk
from login )
group by user_id
)where dt2-dt1=1 group by dt1
order by dt1) a
right join
(
select dt,
       sum(case when rk=1 then 1 else 0 end) new
from 
(select dt, row_number() over(partition by user_id order by dt) rk
from login)
group by dt)b
on a.dt1=b.dt
order by dt

这是用qlsql写的。。
发表于 2021-01-22 02:58:57 回复(0)
比较笨但逻辑比较简单的(mysql)解法:
SELECT t0.d AS `date`, IFNULL(t3.cnt,0) AS p
FROM 
(	-- 为了补上没有新用户的日期
	SELECT DISTINCT(t.date) AS d FROM login t
) t0
LEFT JOIN
(
	SELECT t2.date, ROUND(COUNT(DISTINCT t2.user_id)/ COUNT(DISTINCT(t1.user_id)),3) AS cnt
	FROM (
	-- 得到新用户
		SELECT l1.DATE, l1.user_id
		FROM login AS l1
		WHERE l1.user_id NOT IN 
		(
			SELECT user_id FROM login WHERE DATE < l1.date)
		) t1,
-- 求新用户次日依然留存的用户
		(
		SELECT l1.date, l1.user_id 
		FROM login AS l1 
		WHERE l1.user_id IN
		(
			SELECT user_id	FROM login WHERE DATE= DATE_ADD(l1.DATE, INTERVAL 1 DAY))
		) t2
	WHERE t1.date=t2.date
	GROUP BY t2.date
) t3 
ON t0.d = t3.date
ORDER BY t0.d


发表于 2020-12-04 11:17:44 回复(3)
select all_date,round(ifnull(sum(case when c.log_date=date(c.reg_date,'+1 day') then 1 else 0 end)*1.0/count(distinct(c.user_id)),0),3)
from 
(select distinct(date) as all_date
from login) l 
left join(
    select a.user_id, a.reg_date, b.log_date
    from 
        (select user_id, min(date) as reg_date
        from login
        group by user_id) as a,
        (select user_id, date as log_date
        from login) as b 
    where a.user_id = b.user_id) as c 
on l.all_date = c.reg_date
group by all_date

发表于 2020-09-09 22:00:50 回复(0)
SELECT a.date, ROUND(COUNT(b.user_id) * 1.0/COUNT(a.user_id), 3) AS p
FROM (
    SELECT user_id, MIN(date) AS date
    FROM login
    GROUP BY user_id) a
LEFT JOIN login b
ON a.user_id = b.user_id
AND b.date = date(a.date, '+1 day')
GROUP BY a.date
UNION
SELECT date, 0.000 AS p
FROM login
WHERE date NOT IN (
    SELECT MIN(date)
    FROM login
    GROUP BY user_id)
ORDER BY date;

发表于 2020-08-27 22:46:31 回复(25)

首先明确若要求次日留存率公式为

① 我们用user_id的最小日期作为第一天登录的日期以此来求第一天登录的用户数

select user_id
    ,min(date) as date
from login 
group by user_id

② 用第一天登录的用户与login表联立

并用以下条件表示login 表为第一天登录了第二天也登陆的用户

left join login
on login.user_id = a.user_id
and login.date = date_add(a.date, interval 1 day)

一定要注意此时不包含第一天没有新用户登录的情况!!!需要再有一个有所有日期的表格!!!并且最终的分组要根据该表格的date来分组

完整代码如下所示

select b.date
       ,ifnull(round(count(distinct login.user_id)*1.0 / count(a.user_id),3),0)as p
from (select distinct(date) 
      from login)b
left join 
(select user_id
             ,min(date) as date
      from login 
      group by user_id) a 
on b.date = a.date
left join login
on login.user_id = a.user_id
and login.date  = date_add(a.date, interval 1 day)
group by b.date;
编辑于 2021-02-19 10:11:28 回复(5)
这个题目如果你做过牛客每个人最近的登录日期(三)和牛客每个人最近的登录日期(四),那么解决起来会容易很多,做过(四)https://blog.nowcoder.net/n/a2f955514f824bb888f9d7726421e809我们知道每个日期的新用户求法为:
select login.date,ifnull(n1.new_num,0)
from login 
left join 
(select l1.date,count(distinct l1.user_id) as new_num
from login l1
where l1.date =
(select min(date) from login where user_id=l1.user_id)
group by l1.date) n1
on login.date = n1.date
group by login.date
做过(三) https://blog.nowcoder.net/n/1dad24440b3e45949dfda03cb1f06a9e的话,我们可以轻松得到每个日期新用户次日还登录的人的个数的sql语句如下:
select l1.date,count(distinct l1.user_id)
from login l1
join login l2 on l1.user_id=l2.user_id and l2.date=date((l1.date),'+1 day') 
where l1.date =
(select min(date) from login where user_id=l1.user_id)
group by l1.date;
得到的结果如下:
2020-10-12|2
2020-10-14|1
又在(四)里面提到过联立主表,将查询次日还登录的人的sql也联立主表:
(select login.date,ifnull(n1.new_num,0) as second_login_num
from login 
left join 
(select l1.date,count(distinct l1.user_id) as new_num
from login l1
join login l2 on l1.user_id=l2.user_id and l2.date=date((l1.date),'+1 day') 
where l1.date =
(select min(date) from login where user_id=l1.user_id)
group by l1.date) n1
on login.date = n1.date
group by login.date) second_login
就可以得到:
2020-10-12|2
2020-10-13|0
2020-10-14|1
2020-10-15|0
的结果了,然后将这2个表联立相除,得到的结果保留3为,用ifnull判断0/0的情况,那么就可以得到完整结果了:
select second_login.date, round(ifnull(second_login.second_login_num *1.0/ first_login.first_num,0),3)
from (select login.date,ifnull(n1.new_num,0) as second_login_num
from login 
left join 
(select l1.date,count(distinct l1.user_id) as new_num
from login l1
join login l2 on l1.user_id=l2.user_id and l2.date=date((l1.date),'+1 day') 
where l1.date =
(select min(date) from login where user_id=l1.user_id)
group by l1.date) n1
on login.date = n1.date
group by login.date) second_login

join 

(select login.date,ifnull(n1.new_num,0) as first_num
from login 
left join 
(select l1.date,count(distinct l1.user_id) as new_num
from login l1
where l1.date =
(select min(date) from login where user_id=l1.user_id)
group by l1.date) n1
on login.date = n1.date
group by login.date) first_login

on second_login.date=first_login.date



发表于 2020-08-20 15:09:45 回复(9)
/*看了三天好多题解和讨论区的解法,看的头昏眼花还是理解不了,终于找到这个容易拆解的简单表达方式,希望能降低同学们的理解成本
第一部分是表示新用户登录后,第二天又登录的情况,格式是选取新用户第一天登录的时间,以及对应这个时间的留存率
其实这个表达出的内容,已经是很接近题目要求的答案了
这里理解的难点在于题目要求每个日期新用户的次日留存率,这个留存率套用这一个公式A是不是会有遗漏
那举个栗子如果那天留存率是零,其实是选不出user_id的,是空值,又因为空值是无法代入计算公式的,
其实仔细想想这个公式本来就是回避掉空值只显示第二天有登录的用户才能做计算的
所以我们这个次日留存率的表达式需要完善,可以拆解为二部分
第一部分是新用户登陆后,次日又登录的留存率 A
第二部分是新用户登陆后,次日没有登录的留存率 B
其实A和B存在互斥关系,但它们的结合就是完整集合
举个栗子,一个完整的圆,把她一分为二,A和B部分,她们合起来是才完整集但她们互相排斥
瓶子里水量也可以等于瓶子里的水量,加空气的部分。。。空气的部分也属于瓶子里的空间
映射到题目中第二部分B的值在留存率的分子里外显表现就是零,但这个零的逻辑位置也是我们需要的,
计算机需要我们把所有的逻辑补完才能成功运行
即:留存率=(A+B)/分母 
这里A,B,分母的外显数值和内里逻辑代码都需要一一对应,否则可能会因为内外不一致或者缺失导致误差
很常见的是计算出来的数值都对或者根本不知道怎么搞,其实就是里逻辑没有梳理清楚,只缘身在此山中要跳出来看整体*/

/*下面是代码部分,输入*/
select a.md,round( count(b.user_id)*1.0/ count(a.user_id),3) p
/*a的集合里的user_id就是首次登录的时间对应的user_id数量就是分母
相对的b集合的user_id是关联了a集合里的user_id,且登录时间的限制是在首次登录的基础上
次日又登录了,即分子部分,需要结合上下代码看*/
from (select user_id,min(date) md -- 特别注意这里的别名md
      from login
      group by user_id) a -- 首次登录的时间对应的各个项目
      left join login b -- 首次登录的基础上,次日又登录时间对应的各个项目
      on a.user_id = b.user_id  
      and b.date = date(a.md,'+1 day') -- mysql用date_add(a.md,interval 1 day)
      group by a.md  -- 将首次登录的时间做分组
union  -- 把两个选择select用union联结起来,union联结的每个查询包含相同的列、表达式或聚集函数必须兼容
select date,0.000 p -- 次日没有登录的留存率默认就是零,小数点后三位保持与上面的select格式一致
from login 
where date not in (select min(date)-- 排除(上一个select里排除的时间部分),即那些留存率为零的日期。
      from login
      group by user_id)
-- order by date (测试下来不加也适合本题)

以下是没有注释的样子:
select a.md,round( count(b.user_id)*1.0/ count(a.user_id),3) p
from (select user_id,min(date) md
      from login
      group by user_id) a 
      left join login b 
      on a.user_id = b.user_id  
      and b.date = date(a.md,'+1 day')
      group by a.md 
union 
select date,0.000 p 
where date not in (select min(date)
      from login
      group by user_id)



发表于 2020-10-26 14:21:31 回复(4)
用前后函数加开窗函数:lead(expr,n) #返回分区函数中位于后n行的expr的值 
select 
	date,
	#sum(if(rank1=1,1,0)) #当日新用户总数
	#lead(sum(if(rank1=2,1,0)),1) over(order by date) #次日留存用户总数
	#次日留存率 = 次日留存用户总数/当日用户总数
	round(ifnull(lead(sum(if(rank1=2,1,0)),1) over(order by date)/sum(if(rank1=1,1,0)),0),3) as p
from (select *,row_number() over(partition by user_id order by date) as rank1 from login) as t
group by date 
order by date;


发表于 2021-01-14 19:42:16 回复(10)