SQL 中基于时间的记录分组
Time-based record grouping in SQL
我有一个用户交互数据库 table。我想根据交互的时间和地点创建用户组。也就是说,如果用户在大致相同的时间(例如 2 分钟 window)在同一位置进行交互,我会将他们视为一个组。这些组不需要相互排斥,但它们确实需要详尽无遗。每个用户交互都属于一个或多个组。
我过去用 python 和 做过类似的事情。但现在我仅限于 SQL 解决方案。
假设一个玩具数据table喜欢
create table example_intxns (
intxn_date DATE,
loc_id number,
intxn_timestamp timestamp,
user_id varchar(100)
);
insert into example_intxns (intxn_date, loc_id, intxn_timestamp, user_id)
values
('2021-01-01', 1, '2021-01-01 08:00:00', 'a'),
('2021-01-01', 1, '2021-01-01 08:01:00', 'b'),
('2021-01-01', 1, '2021-01-01 08:02:00', 'c'),
('2021-01-01', 1, '2021-01-01 08:04:00', 'd'),
('2021-01-01', 1, '2021-01-01 08:05:00', 'e'),
('2021-01-01', 1, '2021-01-01 08:07:00', 'f'),
('2021-01-01', 1, '2021-01-01 08:10:00', 'g'),
('2021-01-01', 1, '2021-01-01 08:02:00', 'h')
;
我可以像这样创建一对在 2 分钟内互动的用户
select distinct
a.intxn_date,
a.loc_id,
a.user_id as seed_user_id,
b.user_id
from
example_intxns a
inner join
example_intxns b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(minute, a.intxn_timestamp, b.intxn_timestamp) between -2 and 2
and b.user
此 returns 所有用户对的结果集基于相同的位置和 +/- 2 分钟的互动 window。用户将始终与自己配对——这对于没有其他人在同一时间和地点进行交互的情况很有用。一个用户可以与另一个用户配对。在这里,我使用 b.user_id >= a.user_id
条件,因为 A->B 等同于 B->A。我不需要两者。
但这就是我被困的地方。我不知道如何以 SQL 方式将对扩展到组。感觉可能是递归问题?
我想我想要的——而且我仍在考虑边缘情况——是一个看起来像
的结果集
Date
Location
GroupID
GroupMember
2021-01-01
1
1
a
2021-01-01
1
1
b
2021-01-01
1
1
c
2021-01-01
1
1
h
2021-01-01
1
2
c
2021-01-01
1
2
h
2021-01-01
1
2
d
2021-01-01
1
3
d
2021-01-01
1
3
e
2021-01-01
1
4
e
2021-01-01
1
4
f
2021-01-01
1
5
g
这实际上告诉我,在这个日期,在这个位置,我有 5 个组:[a,b,c,h]、[c,h,d]、[d,e]、[e, f] 和 [g]
解决方案的复杂性包括同一用户在同一天在同一位置多次交互。玩具示例中不存在,但可能存在于实际数据中。
我在 Snowflake 工作,如果这有什么不同的话。现实世界的问题包括每天在六个地点的 10 万名用户,以及大约 5 年的互动。
编辑:
我将目标帖子在我想要的描述上移动了一点。我想要的输出反映了排除作为早期组子集的组出现的需要。也就是说,[b,c] 是一个有效组,但它包含在 [a,b,c,h] 组中。但我并没有特别指出这个标准。我的最终解决方案(包含此附加要求)如下:
with pairs as ( -- qualified pairs
select distinct
a.intxn_date,
a.loc_id,
a.user_id as seed_user_id,
a.intxn_timestamp as seed_intxn_timestamp,
b.user_id,
b.intxn_timestamp as intxn_timestamp,
dense_rank() over (partition by a.intxn_date, a.loc_id order by seed_user_id) as group_id
from
example_entries a
inner join
example_entries b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(second, a.intxn_timestamp, b.intxn_timestamp) between 0 and 120
and b.user_id>=a.user_id
),
groups as ( -- qualified pairs converted into groups
select p.*,
max(p.intxn_timestamp)over(partition by p.group_id) as max_group_intxn_timestamp
from pairs p
),
subtracts as ( -- groups already completely assumed in another, earlier gruop
select a.*
from groups a -- what we want to subtract
inner join groups b
on a.seed_user_id = b.user_id
and a.loc_id = b.loc_id
and a.intxn_date = b.intxn_date
and a.group_id > b.group_id
and timestampdiff(second, a.seed_intxn_timestamp, b.intxn_timestamp) between 0 and 120
inner join groups c
on b.loc_id = c.loc_id
and b.intxn_date = c.intxn_date
and b.seed_intxn_timestamp = c.seed_intxn_timestamp
and b.group_id = c.group_id
and c.user_id >= b.user_id
and b.seed_user_id = c.seed_user_id
and a.user_id = c.user_id
and a.max_group_intxn_timestamp <= c.max_group_intxn_timestamp
)
select distinct a.intxn_date, a.loc_id, a.group_id, a.user_id, a.seed_intxn_timestamp as group_intxn_window_start_timestamp, a.max_group_intxn_timestamp as group_intxn_window_end_timestamp
from groups a
left join (select distinct intxn_date, loc_id, seed_intxn_timestamp, group_id from subtracts) b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and a.seed_intxn_timestamp = b.seed_intxn_timestamp
and a.group_id = b.group_id
where b.group_id is null
/*minus -- could have used a MINUS, but I think the LEFT JOIN way is safer because it removes all traces of the unqualified group
select distinct intxn_date, loc_id, group_id, user_id, seed_intxn_timestamp
from subtracts a*/
order by 1,2,3,4
我稍微修改了您的查询,然后使用 RANK()
:
获得了组 ID
select *, rank() over(partition by intxn_date order by seed_timestamp) grp_id
from (
select a.intxn_date, a.loc_id, a.intxn_timestamp as seed_timestamp, b.user_id
from example_entries a
inner join example_entries b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(minute, a.intxn_timestamp, b.intxn_timestamp) between 0 and 2
and iff(a.intxn_timestamp=b.intxn_timestamp, a.user_id>=b.user_id, true)
)
组id不连续,但是可以看到有7个不同的组,每个组至少有一个元素。有些行属于多个组,但没有没有组的行。
你很接近。只需将此添加到您现有的 select
语句中,即可将组 ID 分配给用户交互。它产生 8 个组,但那是因为它允许组相互包容,这样用户可以每天多次与其他用户交叉(允许 +-2 分钟差异)。它还允许用户与自己交叉,而不管他们是否也与其他用户交叉。
既然你还在考虑边缘情况,我认为这是一个 good/flexible 的开始,应该会让你越过分组障碍。确保您的数据在 join
之前被删除了重复数据
dense_rank() over (partition by a.intxn_date, a.loc_id order by a.user_id) as group_id
另一种方法可能如下:
首先获取所有可能的位置|时间组合(又名 groupys)。
简单的左外连接。完成!
我还注意到 'correct' 回答中 8:02 的用户数应该是 3,而不是报告的 5。
代码|复制|粘贴|运行:
select
groupys.groups
, groupys.loc_id
, example_intxns.user_id
, groupys.starts
from
( select
row_number() over (order by intxn_timestamp) groups
, loc_id
, intxn_timestamp starts
, intxn_timestamp + interval '2 minutes' ends
from
example_intxns
group by
2,3,4) groupys
left outer join
example_intxns
on example_intxns.loc_id = groupys.loc_id
and example_intxns.intxn_timestamp between groupys.starts and groupys.ends
我有一个用户交互数据库 table。我想根据交互的时间和地点创建用户组。也就是说,如果用户在大致相同的时间(例如 2 分钟 window)在同一位置进行交互,我会将他们视为一个组。这些组不需要相互排斥,但它们确实需要详尽无遗。每个用户交互都属于一个或多个组。
我过去用 python 和
假设一个玩具数据table喜欢
create table example_intxns (
intxn_date DATE,
loc_id number,
intxn_timestamp timestamp,
user_id varchar(100)
);
insert into example_intxns (intxn_date, loc_id, intxn_timestamp, user_id)
values
('2021-01-01', 1, '2021-01-01 08:00:00', 'a'),
('2021-01-01', 1, '2021-01-01 08:01:00', 'b'),
('2021-01-01', 1, '2021-01-01 08:02:00', 'c'),
('2021-01-01', 1, '2021-01-01 08:04:00', 'd'),
('2021-01-01', 1, '2021-01-01 08:05:00', 'e'),
('2021-01-01', 1, '2021-01-01 08:07:00', 'f'),
('2021-01-01', 1, '2021-01-01 08:10:00', 'g'),
('2021-01-01', 1, '2021-01-01 08:02:00', 'h')
;
我可以像这样创建一对在 2 分钟内互动的用户
select distinct
a.intxn_date,
a.loc_id,
a.user_id as seed_user_id,
b.user_id
from
example_intxns a
inner join
example_intxns b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(minute, a.intxn_timestamp, b.intxn_timestamp) between -2 and 2
and b.user
此 returns 所有用户对的结果集基于相同的位置和 +/- 2 分钟的互动 window。用户将始终与自己配对——这对于没有其他人在同一时间和地点进行交互的情况很有用。一个用户可以与另一个用户配对。在这里,我使用 b.user_id >= a.user_id
条件,因为 A->B 等同于 B->A。我不需要两者。
但这就是我被困的地方。我不知道如何以 SQL 方式将对扩展到组。感觉可能是递归问题?
我想我想要的——而且我仍在考虑边缘情况——是一个看起来像
的结果集Date | Location | GroupID | GroupMember |
---|---|---|---|
2021-01-01 | 1 | 1 | a |
2021-01-01 | 1 | 1 | b |
2021-01-01 | 1 | 1 | c |
2021-01-01 | 1 | 1 | h |
2021-01-01 | 1 | 2 | c |
2021-01-01 | 1 | 2 | h |
2021-01-01 | 1 | 2 | d |
2021-01-01 | 1 | 3 | d |
2021-01-01 | 1 | 3 | e |
2021-01-01 | 1 | 4 | e |
2021-01-01 | 1 | 4 | f |
2021-01-01 | 1 | 5 | g |
这实际上告诉我,在这个日期,在这个位置,我有 5 个组:[a,b,c,h]、[c,h,d]、[d,e]、[e, f] 和 [g]
解决方案的复杂性包括同一用户在同一天在同一位置多次交互。玩具示例中不存在,但可能存在于实际数据中。
我在 Snowflake 工作,如果这有什么不同的话。现实世界的问题包括每天在六个地点的 10 万名用户,以及大约 5 年的互动。
编辑: 我将目标帖子在我想要的描述上移动了一点。我想要的输出反映了排除作为早期组子集的组出现的需要。也就是说,[b,c] 是一个有效组,但它包含在 [a,b,c,h] 组中。但我并没有特别指出这个标准。我的最终解决方案(包含此附加要求)如下:
with pairs as ( -- qualified pairs
select distinct
a.intxn_date,
a.loc_id,
a.user_id as seed_user_id,
a.intxn_timestamp as seed_intxn_timestamp,
b.user_id,
b.intxn_timestamp as intxn_timestamp,
dense_rank() over (partition by a.intxn_date, a.loc_id order by seed_user_id) as group_id
from
example_entries a
inner join
example_entries b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(second, a.intxn_timestamp, b.intxn_timestamp) between 0 and 120
and b.user_id>=a.user_id
),
groups as ( -- qualified pairs converted into groups
select p.*,
max(p.intxn_timestamp)over(partition by p.group_id) as max_group_intxn_timestamp
from pairs p
),
subtracts as ( -- groups already completely assumed in another, earlier gruop
select a.*
from groups a -- what we want to subtract
inner join groups b
on a.seed_user_id = b.user_id
and a.loc_id = b.loc_id
and a.intxn_date = b.intxn_date
and a.group_id > b.group_id
and timestampdiff(second, a.seed_intxn_timestamp, b.intxn_timestamp) between 0 and 120
inner join groups c
on b.loc_id = c.loc_id
and b.intxn_date = c.intxn_date
and b.seed_intxn_timestamp = c.seed_intxn_timestamp
and b.group_id = c.group_id
and c.user_id >= b.user_id
and b.seed_user_id = c.seed_user_id
and a.user_id = c.user_id
and a.max_group_intxn_timestamp <= c.max_group_intxn_timestamp
)
select distinct a.intxn_date, a.loc_id, a.group_id, a.user_id, a.seed_intxn_timestamp as group_intxn_window_start_timestamp, a.max_group_intxn_timestamp as group_intxn_window_end_timestamp
from groups a
left join (select distinct intxn_date, loc_id, seed_intxn_timestamp, group_id from subtracts) b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and a.seed_intxn_timestamp = b.seed_intxn_timestamp
and a.group_id = b.group_id
where b.group_id is null
/*minus -- could have used a MINUS, but I think the LEFT JOIN way is safer because it removes all traces of the unqualified group
select distinct intxn_date, loc_id, group_id, user_id, seed_intxn_timestamp
from subtracts a*/
order by 1,2,3,4
我稍微修改了您的查询,然后使用 RANK()
:
select *, rank() over(partition by intxn_date order by seed_timestamp) grp_id
from (
select a.intxn_date, a.loc_id, a.intxn_timestamp as seed_timestamp, b.user_id
from example_entries a
inner join example_entries b
on a.intxn_date = b.intxn_date
and a.loc_id = b.loc_id
and timestampdiff(minute, a.intxn_timestamp, b.intxn_timestamp) between 0 and 2
and iff(a.intxn_timestamp=b.intxn_timestamp, a.user_id>=b.user_id, true)
)
组id不连续,但是可以看到有7个不同的组,每个组至少有一个元素。有些行属于多个组,但没有没有组的行。
你很接近。只需将此添加到您现有的 select
语句中,即可将组 ID 分配给用户交互。它产生 8 个组,但那是因为它允许组相互包容,这样用户可以每天多次与其他用户交叉(允许 +-2 分钟差异)。它还允许用户与自己交叉,而不管他们是否也与其他用户交叉。
既然你还在考虑边缘情况,我认为这是一个 good/flexible 的开始,应该会让你越过分组障碍。确保您的数据在 join
dense_rank() over (partition by a.intxn_date, a.loc_id order by a.user_id) as group_id
另一种方法可能如下:
首先获取所有可能的位置|时间组合(又名 groupys)。
简单的左外连接。完成!
我还注意到 'correct' 回答中 8:02 的用户数应该是 3,而不是报告的 5。
代码|复制|粘贴|运行:
select
groupys.groups
, groupys.loc_id
, example_intxns.user_id
, groupys.starts
from
( select
row_number() over (order by intxn_timestamp) groups
, loc_id
, intxn_timestamp starts
, intxn_timestamp + interval '2 minutes' ends
from
example_intxns
group by
2,3,4) groupys
left outer join
example_intxns
on example_intxns.loc_id = groupys.loc_id
and example_intxns.intxn_timestamp between groupys.starts and groupys.ends