牛客SQL大厂面试真题之用户增长场景

好久没写sql语句了,今天给大家分析下牛客网的SQL大厂真题。想要了解更多关于mysql的函数等知识,可以光临我的SQL专栏,含有窗口函数、日期函数及频繁使用的知识点等:

博客主页博客主页

SQL集合专栏SQL集合专栏

文章目录

  • 0、场景及数据字段说明
  • 1、统计2021年11月每天的人均浏览文章时长(秒数)
  • 2、每篇文章同一时刻最大在看人数
  • 3、2021年11月每天新用户的次日留存率(保留2位小数)
  • 4、统计活跃间隔对用户分级结果
  • 5、每天的日活数及新用户占比
  • 6、连续签到领金币

0、场景及数据字段说明

  • 现有一张表tb_user_log,字段及说明如下表所示:
artical_id-文章ID代表用户浏览的文章的ID,ID为0表示用户在非文章内容页(比如App内的列表页、活动页等)。
字段 数据类型 解释
id INT 自增ID
uid INT 用户ID
artical_id INT 文章ID
in_time datetime 进入时间
out_time datetime 离开时间
sign_in TINYINT 是否签到

具体题目详情可以参考牛客官网

1、统计2021年11月每天的人均浏览文章时长(秒数)

  • 结果保留1位小数,并按时长由短到长排序。
思路:
首先where筛选出符合时间的数据,根据日期(天)分组;
人均浏览时长 = 所有人浏览时长和 / 人数(去重) ;
时长单位为秒数,利用时间戳函数timestampdiff(日期单元, start_time, end_time)
select date_format(out_time,'%Y-%m-%d') as dt
	 ,round(sum(TIMESTAMPDIFF(second,in_time,out_time)) / count(distinct uid),1) as avg_view
from tb_user_log
where date_format(out_time,'%Y-%m') = '2021-11'
group by dt 
order by avg_view asc

2、每篇文章同一时刻最大在看人数

  • 统计每篇文章同一时刻最大在看人数,如果同一时刻有进入也有离开时,先记录用户数增加再记录减少,结果按最大人数降序。
  • 这种题,做过一道就会了所有同时,例如之前我的滴滴打车面试题目的详解博客中就有一道各城市同时等车的最大人数,思路都是一样的。
思路:
- 题目中也说了同一时刻有进有出,因此我们首先可以以1代表进入,-1代表离开,方便加减法运算。
- 首先将原表分成两张表,一张进入,一张离开,并加入新字段in_out(进入1离开-1),进入表格的in_out=1,离开表格的in_out=-1;
- 继而将两张表纵向合并;合并后字段包含uid ,artical_id ,dt(进入或离开的时间), in_out;
- 求的是每篇文章,因此我先根据文章分区(注意:不是分组,分组的话就无法计算各时刻的同时在看人数了) 
,按照时间升序排序,对in_out进行sum求和(因为order by了,所以这里的和是指截至当前时刻的在看人数) 
- 计算完每篇文章各时刻的在线人数后,可以对文章id进行分组了(因为只要人数最多的那个时刻),取同时在读人数最大的那个person_online就ok了。
select artical_id, max(person_online) as max_person_online
from (
	select artical_id 
		,dt 
		,in_out 
		,sum(in_out) over(partition by artical_id order by dt asc ,in_out desc) as person_online    
		# 这里的in_out降序别忘了,因为题目中 “先记录用户数增加再记录减少”
	from (
		(select uid ,artical_id ,in_time as dt, '1' as in_out from tb_user_log)
		union all
		(select uid ,artical_id ,out_time as dt, '-1' as in_out from tb_user_log)
		) a 
	) b 
group by artical_id
order by max_person_online desc

3、2021年11月每天新用户的次日留存率(保留2位小数)

  • 次日留存率为当天新增的用户数中第二天又活跃了的用户数占比。
  • 如果in_time-进入时间和out_time-离开时间跨天了,在两天里都记为该用户活跃过,结果按日期升序。
做出来了,但过程有点复杂,又参考了其他牛客小伙伴的答案,两种我都说一说。
思路:
- 查询各用户首次进入的日期(a表),查询各用户所有活跃日期(b表),按照用户id进行表连接;
- 根据a表的首次进入时间分组,对a表的用户去重计数即为当日的新用户数;
- 对b表的用户去重计数即为次日活跃用户数(不含新增,因为次日,所以日期需满足a表的日期+1)
- 次日留存率 = 次日活跃人数 / 当天新增用户数 

select  a.first_day 
	  ,count(distinct a.uid) new_num# 当天的新用户数
	  ,count(distinct (case when datediff(b.dt,a.first_day) = 1 then b.uid end)) as sen_num # 次日用户数
	  ,round(count(distinct (case when datediff(b.dt,a.first_day) = 1 then b.uid end)) / count(distinct a.uid),2) p # 次日留存率
from (
		select uid, min(date_format(out_time,'%Y-%m-%d')) as first_day
		from tb_user_log
		group by uid 
		) a  # 各用户的第一次进入的日期
left join (
		(select uid ,artical_id ,date_format(in_time,'%Y-%m-%d') as dt from tb_user_log) 
		union
		(select uid ,artical_id ,date_format(out_time,'%Y-%m-%d') as dt from tb_user_log)
		) b # 可能有跨天的用户,因此将其连接成一列日期
on a.uid = b.uid 		
where date_format(a.first_day,'%Y-%m') = '2021-11'
group by a.first_day 
order by a.first_day asc
  • 这个方法没想到,我见到还是比较少,可能题量还是不够吧。
这种方法是在连接条件上做了手脚,上面的方法仅仅根据用户id连接,但这里除了id,还有日期上的条件,
因为题目说了次日,所以我们可以仅仅连接那些次日活跃了用户 即条件为 首次进入时间 + 1 = 次日时间;
这里次日时间需要条件判断,因为有跨天,单单连接in_time或者out_time都不对,因此用if()语句判断
如果 date(in_time) = date(out_time),说明同一天,返回哪个都可以;
如果date(in_time)+1 = date(out_time),说明跨天了,需返回第二天,因为第一天是首次进入的日期了 

select a.min_date,round(count(distinct b.uid)/count(distinct a.uid),2) p
from (
	select uid,min(date(in_time)) min_date
	from tb_user_log 
	group by uid ) a
left join tb_user_log b 
on a.uid = b.uid
and a.min_date = if(date(in_time) = DATE_ADD(date(out_time),INTERVAL -1 DAY),DATE_ADD(date(out_time),INTERVAL -1 DAY),DATE_ADD(date(in_time),INTERVAL -1 DAY))
where DATE_FORMAT(a.min_date,'%Y-%m') = '2021-11'
group by a.min_date 

4、统计活跃间隔对用户分级结果

  • 统计活跃间隔对用户分级后,各活跃等级用户占比,结果保留两位小数,且按占比降序排序。
  • 用户等级标准简化为:忠实用户(近7天活跃过且非新晋用户)、新晋用户(近7天新增)、沉睡用户(近7天未活跃但更早前活跃过)、流失用户(近30天未活跃但更早前活跃过)。
    假设今天就是数据中所有日期的最大值。近7天表示包含当天T的近7天,即闭区间[T-6, T]。
思路:
首先按照用户等级标准对用户分层:将其作为一列
	新晋用户:最大日期与用户的首次进入日期(新增)的差<7,
	沉睡用户:最大日期与最近一次进入日期(活跃日期)的差 为7—30
	流失用户:最大日期与最近一次进入日期的差>=30 
	其余皆为忠实用户(最大日期与最近一次进入日期差<7且与首次进入日期差>=7)
等级占比 = 各层级用户数 / 总用户数  
select  case when datediff((select max(date(out_time)) from tb_user_log),first_day) < 7 then '新晋用户'
			 when datediff((select max(date(out_time)) from tb_user_log),last_day) between 7 and 30 then '沉睡用户'
			 when datediff((select max(date(out_time)) from tb_user_log),last_day) >=30 then '流失用户'
			 else '忠实用户' end as user_grade
	   ,round(count(distinct a.uid) / (select count(distinct uid) from tb_user_log),2) as ratio				
from (select uid,max(date(out_time)) last_day from tb_user_log GROUP BY uid) a 
left join (select uid,min(date(out_time)) first_day from tb_user_log GROUP BY uid) b
on a.uid = b.uid
group by  user_grade  # 分组字段可以直接使用字段,因为case when 优于 group by
order by ratio desc

5、每天的日活数及新用户占比

  • 新用户占比=当天的新用户数÷当天活跃用户数(日活数)。
    如果in_time-进入时间和out_time-离开时间跨天了,在两天里都记为该用户活跃过。
    新用户占比保留2位小数,结果按日期升序排序。
思路:
由于跨天,所以和第锕题一样,还是先将各用户的活跃时间和首次进入时间按照用户id连接;
根据活跃日期(不是首次进入日期)分组,对用户id去重计数 为日活;
按照活跃日期=首次进入时间的条件对用户id去重计数为当天新增用户,再除以 日活 即为新用户占比
select a.dt
	  ,count(DISTINCT a.uid) as DAU
	  ,round(count(DISTINCT case when a.dt = b.first_day then a.uid end)/count(DISTINCT a.uid),2) as new_user_ratio		
from (
	(select uid ,artical_id ,date_format(in_time,'%Y-%m-%d') as dt from tb_user_log)
	union
	(select uid ,artical_id ,date_format(out_time,'%Y-%m-%d') as dt from tb_user_log)
) a 
left join (select uid,min(date(in_time)) as first_day from tb_user_log group by uid) b 
on a.uid = b.uid  
group by a.dt 
order by new_user_ratio asc

6、连续签到领金币

  • 计算每个用户2021年7月以来每月获得的金币数(该活动到10月底结束,11月1日开始的签到不再获得金币)。结果按月份、ID升序排序。
    场景逻辑说明:
    注意:只有artical_id为0时sign_in值才有效。
    从2021年7月7日0点开始,用户每天签到可以领1金币,并可以开始累积签到天数,连续签到的第3、7天分别可额外领2、6金币。每连续签到7天后重新累积签到天数(即重置签到天数:连续第8天签到时记为新的一轮签到的第一天,领1金币)注:如果签到记录的in_time-进入时间和out_time-离开时间跨天了,也只记作in_time对应的日期签到了。

  • 与连续签到问题和连续登录问题是类似的,具体详解见下面

select uid,date_format(dt,'%Y%m') month,sum(if(days=3,3,if(days=0,7,1))) coin # 如果连续3天返回 3金币,若连续7天,返回7金币,否则都是1金币
from(
    select uid,dt, mod(row_number() over(partition by uid,start_day order by dt),7) days
    from(
        select uid
              ,date(in_time) dt
              ,date_sub(date(in_time),interval row_number()over(partition by uid order by date(in_time)) day) start_day
        from tb_user_log
        where artical_id=0 
        and sign_in=1
        and date(in_time) between '2021-07-07' and '2021-10-31' 
    	) t2  # 1、对用户分区,日期升序排序,用日期-排序=新日期组 同一组即是连续的日期
	) t3 # 2、对用户和日期组分区,原日期升序,得到连续签到几天了; 除以7代表 签到七天后重新按第一天登录算
group by uid,month
order by month,uid

你可能感兴趣的:(MYSQL,面试,sql,数据分析,mysql)