SQL面试题——连续出现次数
其实前面关于连续问题,我们遇到过,就是最大连续登陆天数,或者是连续登陆天数的计算,可以参考我们之前的文章
SQL面试题——最大连续登陆问题
最大连续登陆问题
最大连续登陆问题的难点在于如何判断连续,其实实现原理很简单,我们首先让每个用户每一天只有一条数据,然后按照用户分区,按照时间做row_number() 排序。
例如下面的日期是某个用户的登陆日期,编号就是row_number() 返回的排序值
日期 | 编号 | 差值(n 天前) |
---|---|---|
2021-06-10 | 1 | 2021-06-09 |
2021-06-11 | 2 | 2021-06-09 |
2021-06-12 | 3 | 2021-06-09 |
2021-06-14 | 4 | 2021-06-10 |
2021-06-15 | 5 | 2021-06-10 |
为了方便我在上面举了一个例子,假设一个用户在2021-06-10 到2021-06-12 连续登陆了3天,那么它们的差值都是2021-06-09号,但是在2021-06-13 号断了一天,然后又在2021-06-14到2021-06-15连续登陆了2天,它们的差值都是2021-06-10
也就是说连续登陆的天数就等于不同差值的个数
- 用户在2021-06-10号开始连续登陆了3天,它们的日期差是2021-06-09
- 用户在2021-06-14号开始连续登陆了2天,它们的日期差是2021-06-10
上面就是我们做最大连续登陆问题的关键
- 去重排序,每个用户每天只有一条数据
- 计算日期差值,连续登陆的天数就等于不同差值的个数
- 最大连续登陆,就是取个数最多的那个日期差值的个数
连续得分问题背景
这个问题的背景是这样的,勇士和湖人的西部半决赛正在进行中,有这样一张表basketball_score记录了每个球员的得分情况,我们的需求是找出连续4次(及以上)为球队得分的球员
这里的需求描述为球队肯定是指的是球队内连续4次(及以上)为球队得分的球员,也就是说在一个球队内连续4次以上得分的球员
+----+------+-----------+-----+-------------------+
|team|number|player_name|score| get_score_time|
+----+------+-----------+-----+-------------------+
|勇士| 30| 库里| 3|2023-05-11 11:01:14|
|勇士| 30| 库里| 2|2023-05-11 11:02:17|
|湖人| 23| 詹姆斯| 1|2023-05-11 11:06:14|
|勇士| 30| 库里| 3|2023-05-11 11:07:12|
|湖人| 11| 戴维斯| 1|2023-05-11 11:08:12|
|勇士| 30| 库里| 3|2023-05-11 11:08:45|
|勇士| 11| 汤普森| 3|2023-05-11 11:09:11|
|勇士| 22| 维金斯| 3|2023-05-11 11:09:45|
|湖人| 1| 拉塞尔| 1|2023-05-11 11:10:27|
|湖人| 23| 詹姆斯| 1|2023-05-11 11:11:12|
|湖人| 23| 詹姆斯| 1|2023-05-11 11:14:12|
+----+------+-----------+-----+-------------------+
这是一道来自知乎帖子的问题,这里我们也看一些博主的解题思路
- 首先肯定是需要按球队分组
- 我们很容易能想到,如果按照得分时间排序后,能将得分球员的名字也组成单独的列,如果一行数据中,4列的球员名字都相同,该球员就是连续4次为球队得分的球员
- 根据这个思路,我们用偏移窗口函数lead(),分别取后一行,2行,3行的球员名字值,分别组成单独的列
感觉不是很好理解的话,看一下代码就立马懂了
select team,player_name,lead(player_name,1)over(partition by team order by get_score_time) as player_name1,lead(player_name,2)over(partition by team order by get_score_time) as player_name2,lead(player_name,3)over(partition by team order by get_score_time) as player_name3from basketball_score
就是player_name和player_name1,player_name2,player_name3 都相等的就是连续4次得分的球员,完整代码如下
select distinct team,player_name-- distinct语法规定对单字段、多字段去重,必须放在第一个查询字段前-- 如果对表中多列字段进行去重,去重的过程就是将多字段作为整体去重
from
(select team,player_name,lead(player_name,1)over(partition by team order by get_score_time) as player_name1,lead(player_name,2)over(partition by team order by get_score_time) as player_name2,lead(player_name,3)over(partition by team order by get_score_time) as player_name3from basketball_score
) as t1
where t1.player_name=t1.player_name1
and t1.player_name=t1.player_name2
and t1.player_name=t1.player_name3
能不能解决这个问题呢,能,优雅不,不优雅,简单不,简单
问题出在哪里了,就只这里只计算了连续得分4次的,没有计算出4次以上的,如果想计算连续10次得分呢,需要增加9列吗?
连续得分问题解决思路
这个和连续登陆的问题不一样的地方,就是我们做不出来日期差值这样的一个判断连续的标志。所以我们需要构造这样的一个的一个列去判断连续,只要这个值相同就是连续,相同值的个数就是连续的次数,这和连续登陆问题的思路一样。
虽然我们不知道怎么利用球员和排序值构造出来这样一个列或者标志,但是我们知道一定会用到这个排序值的,最大连续登陆也用到了
selectteam,player_name,row_number() over(partition by team order by get_score_time) as rnfrombasketball_score
其实观察这个数据我们发现,我们应该给四个连续的库里一个相同的值,也就是说我们给连续的值的标志是这个连续序列的第一个序号,可以参考詹姆斯的,当不连续的时候就给对应单独的
如果能做到这样,就相当于我们构造出来了这个判断连续的列。现在的问题是我们怎么实现这个,其实我们可以这样,再构造一列获取上一次得分的球员,我们用库里这里举例子,我们会得到null,库里,库里,库里,库里 这样的一个序列,我们看一下数据
selectteam,get_score_time,player_name,lag(player_name,1)over(partition by team order by get_score_time) as lag_player_name,row_number() over(partition by team order by get_score_time) as rn
frombasketball_score
这个时候我们可以让player_name和lag_player_name 相等的用null 填充,就是这次和上一次得分的是同一个人的
selectteam,get_score_time,player_name,lag(player_name,1)over(partition by team order by get_score_time) as lag_player_name,if(player_name=lag_player_name,null,row_number() over(partition by team order by get_score_time) ) as new_rn ,row_number() over(partition by team order by get_score_time) as rn
frombasketball_score
可以观察我们构造出来的new_rn,这下我们只要用前面一个不是null 的new_rn 去填充是null 的,即可构造出来这样的一个标志列
selectteam,get_score_time,player_name,last(new_rn,true) over(partition by team order by get_score_time) as new_rn,rn
from (selectteam,get_score_time,player_name,lag(player_name,1)over(partition by team order by get_score_time) as lag_player_name,if(player_name=lag_player_name,null,row_number() over(partition by team order by get_score_time) ) as new_rn ,row_number() over(partition by team order by get_score_time) as rnfrombasketball_score)
到这里我们的目标列就构造出来了,接下来就来实现业务需求,这下和连续登陆一样了
selectteam,new_rn,max(player_name) as player_name-- 首次得分时间,min(get_score_time) as first_get_score_time-- 连续得分次数,count(1) as score_times
from(
selectteam,get_score_time,player_name,last(new_rn,true) over(partition by team order by get_score_time) as new_rn,rn
from (selectteam,get_score_time,player_name,lag(player_name,1)over(partition by team order by get_score_time) as lag_player_name,if(player_name=lag_player_name,null,row_number() over(partition by team order by get_score_time) ) as new_rn ,row_number() over(partition by team order by get_score_time) as rnfrombasketball_score)
)
group byteam,new_rn
havingscore_times>=4
总结
连续问题不论是连续登陆还是连续出现,我们的核心逻辑是构造判断连续的列,而这个列的构造依赖的是根据时间的排序值