好久没写sql语句了,今天给大家分析下牛客网的SQL大厂真题。想要了解更多关于mysql的函数等知识,可以光临我的SQL专栏,含有窗口函数、日期函数及频繁使用的知识点等:
博客主页: 博客主页
SQL集合专栏:SQL集合专栏
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 | 是否签到 |
具体题目详情可以参考牛客官网
思路:
首先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
思路:
- 题目中也说了同一时刻有进有出,因此我们首先可以以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
做出来了,但过程有点复杂,又参考了其他牛客小伙伴的答案,两种我都说一说。
思路:
- 查询各用户首次进入的日期(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
思路:
首先按照用户等级标准对用户分层:将其作为一列
新晋用户:最大日期与用户的首次进入日期(新增)的差<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
思路:
由于跨天,所以和第锕题一样,还是先将各用户的活跃时间和首次进入时间按照用户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
计算每个用户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