Description
题目
题目连接:活跃用户
写一个 SQL 查询, 找到活跃用户的 id
和 name
。
活跃用户是指那些至少连续 5
天登录账户的用户。
create table accounts (id int, name varchar(10));
insert into accounts (id, name) values
(1, 'Winston'),
(7, 'Jonathan');
create table logins (id int, login_date date);
insert into logins (id, login_date) values
(7, '2020-05-30'),
(1, '2020-05-30'),
(7, '2020-05-31'),
(7, '2020-06-01'),
(7, '2020-06-02'),
(7, '2020-06-02'),
(7, '2020-06-03'),
(1, '2020-06-07'),
(7, '2020-06-10');
SQL:方法一
select
distinct id, name
from (
select
id,
login_date,
datediff(lead(login_date, 4) over(partition by id order by login_date), login_date) diff
from logins group by id, login_date
) temp left join accounts using(id) where diff = 4
解析
-
去重,因为一名用户可能会在一天当中登录多次
select id, login_date from logins group by id, login_date
-
使用
lead
窗口函数,向下4
行lead(login_date, 4) over(partition by id order by login_date) -- 结果 id | login_date | lead_date 1 | 2020-05-30 | null 1 | 2020-06-07 | null 7 | 2020-05-30 | 2020-06-03 7 | 2020-05-31 | 2020-06-10 7 | 2020-06-01 | null 7 | 2020-06-02 | null 7 ] 2020-06-03 | null 7 | 2020-06-10 | null
-
使用
datediff
计算login_date
和lead_date
之间的时间差 -
将上面的结果作为临时表
temp
-
将临时表
temp
和accounts
左连,筛选出第三步中的时间差为4
的用户
SQL:方法二
select
distinct id, name
from accounts join (
select
id,
@rn:=if(@pre_id=id and @pre_login_date = subdate(login_date, interval 1 day), @rn + 1, 1) rn,
@pre_id:=id,
@pre_login_date:=login_date
from (
select id, login_date from logins group by id, login_date
) t1, (select @pre_id:=null, @pre_login_date:=null, @rn:=0) init
) t2 using(id) where t2.rn >= 5 order by accounts.id;
解析
使用变量对将每个用户的登录日期(去重)按照先后顺序进行排名,
- 比较当前数据的
id
和 上一条数据的id
是否相等 - 用
subdate
比较当前数据的login_date
和上一条数据的login_date
是否相等
-
各变量初始化
@pre_id
保存上一条数据的id
, 初始化为null
@pre_login_date
保存上一条数据的login_date
, 初始化为null
@rn
保存媒体数据的排名,初始化为0
select @pre_id:=null, @pre_login_date:=null, @rn:=0
-
使用
if
进行判断- 如果
@pre_id
(上一条数据的id
) 等于当前这条数据的id
,并且@pre_login_date
(上一条数据的login_date
)等于当前这条数据的login_date
,输出@rn + 1
,否则输出1
- 运行第一条数据时,
@pre_id = null
、@pre_login_date = null
@pre_id = id
是null = 7
为false
@pre_login_date = subdate(login_date, interval 1 day)
是null = '2020-05-29'
为false
- 输出为
1
- 将
@pre_id
赋值为当前数据的id
,结果是@pre_id:=7
;@pre_login_date
赋值为当前数据的login_date
,结果是@pre_login_date:= '2020-05-30'
- 运行第二条数据时,
@pre_id = 7
、@pre_login_date = '2020-05-30'
@pre_id = id
是7 = 7
为true
@pre_login_date = subdate(login_date, interval 1 day)
是'2020-05-30' = '2020-05-30'
为true
- 输出
@rn + 1
为2
- 将
@pre_id
赋值为当前数据的id
,结果是@pre_id:=7
;@pre_login_date
赋值为当前数据的login_date
,结果是@pre_login_date:= '2020-05-31'
- 以此类推,运行第三条数据,直到运行到最后一条数据
- 运行第一条数据时,
if(@pre_id=id and @pre_login_date = subdate(login_date, interval 1 day), @rn + 1, 1) rn
- 如果
-
将步骤一和步骤二的结果作为临时表
t2
-
查询临时表
t2
筛选出rn
大于5
的数据。
SQL:方法三
select
distinct id, name
from (
select
id,
subdate(login_date, rn) first_login_date
from (
select
id,
login_date,
row_number() over(partition by id order by login_date) rn
from (
select
id,
login_date
from logins group by id, login_date order by id, login_date
) t1
) t2 group by id, first_login_date having count(*) >= 5
) t3 left join accounts using(id);
解析
使用窗口函数对将每个用户的登录日期(去重)按照先后顺序进行排名,再用 subdate
计算出第一天登录日期
-
使用
row_number()
,将每个用户按照登录日期进行排序,因为一个用户可以一天登录多次,所以所以首先要对logins.login_date
进行去重select id, login_date, row_number() over(partition by id order by login_date) rn from ( select id, login_date from logins group by id, login_date ) t1
-
使用
subdate
计算每个日期的第一次登录的日期- 计算一段连续的数:用一个数(日期)减去它的排名,会得到一个相同的数
select id, subdate(login_date, rn) first_login_date from ( select id, login_date, row_number() over(partition by id order by login_date) rn from ( select id, login_date from logins group by id, login_date ) t1 ) t2
-
按照
id
和first_login_date
进行分组,筛选出大于5
天的用户 -
连接
accounts
,再对accounts.id
和accounts.name
进行去重- 去重是因为,某一个用户可能会有几个连续登录
5
天的区间
- 去重是因为,某一个用户可能会有几个连续登录
SQL:方法四
select
distinct accounts.*
from accounts
join logins l1 using(id)
join logins l2 on l1.id = l2.id and datediff(l2.login_date, l1.login_date) between 0 and 4
group by accounts.id, accounts.name, l1.login_date
having count(distinct l2.login_date) = 5;
解析
自连接, l1.login_date
作为登录起始日, l2.login_date
用来和 l1.login_date
进行比较,也就是说每个日期都检查之后是否有 4
个连续日期
-
datediff(l2.login_date, l1.login_date)
进行比较,是否在0
到4
天之间datediff('2022-01-02', '2022-01-02') // 0
datediff('2022-01-03', '2022-01-02') // 1
datediff('2022-01-04', '2022-01-02') // 2
datediff('2022-01-05', '2022-01-02') // 3
datediff('2022-01-06', '2022-01-02') // 4
那你可能会想到可不可以用
between 1 and 5
,那我们来看一下1 ~ 5
的结果是啥datediff('2022-01-03', '2022-01-02') // 1
datediff('2022-01-04', '2022-01-02') // 2
datediff('2022-01-05', '2022-01-02') // 3
datediff('2022-01-06', '2022-01-02') // 4
datediff('2022-01-07', '2022-01-02') // 5
l2.login_date
是l1.login_date
开始日后(包括开始日)连续登录日期,如果l2.login_date
不包括开始日,后面处理就麻烦了。select * from logins l1 join logins l2 on l1.id = l2.id and datediff(l2.login_date, l1.login_date) between 0 and 4;
-
将
accounts
和l1
、l2
连接,并且按照accounts.id
、accounts.name
、l1.login_date
进行分组。- 为什么这里使用
l1.login_date
,因为第一步中计算l1.login_date
后是否有4
个连续日期
- 为什么这里使用
-
使用
having
筛选出l2.login_date
等于5
数据(需要使用distinct
去重)- 用户可以在一天之内登录多次,所有
l2.login_date
不去重的话,不符合题目要求
- 用户可以在一天之内登录多次,所有