跳到主要内容

SQL 互相关注的用户对:分组聚合找出双向关注(快手面试题)

一、题目

已知有表 t5_follow_record 记录了用户之间的关注关系,包含字段:follower_id(关注者ID)、followee_id(被关注者ID)、follow_time(关注时间)。

请找出所有互相关注的用户对(即 A 关注了 B,同时 B 也关注了 A)。输出结果中每对用户只出现一次,即 (A, B) 和 (B, A) 视为同一对。

样例数据

+--------------+--------------+----------------------+
| follower_id | followee_id | follow_time |
+--------------+--------------+----------------------+
| u001 | u002 | 2024-06-01 08:00:00 |
| u002 | u001 | 2024-06-02 09:00:00 |
| u001 | u003 | 2024-06-01 10:00:00 |
| u003 | u002 | 2024-06-03 11:00:00 |
| u002 | u003 | 2024-06-04 12:00:00 |
| u004 | u001 | 2024-06-01 13:00:00 |
| u001 | u004 | 2024-06-05 14:00:00 |
| u005 | u001 | 2024-06-06 15:00:00 |
| u003 | u004 | 2024-06-02 16:00:00 |
+--------------+--------------+----------------------+

二、思路分析

本题是社交网络关系分析中的经典问题。核心思路是把有向的关注关系归一化为无向的用户对,再用分组聚合判断双向:

  1. 归一化用户对:用 least(follower_id, followee_id)greatest(follower_id, followee_id) 把每条关注关系映射成无向用户对 (较小ID, 较大ID)。无论 A 关注 B 还是 B 关注 A,都归到同一个 (A, B) 对。
  2. 分组统计方向数:按归一化后的用户对 GROUP BY,用 count(distinct follower_id) 统计这对用户中有几个不同的关注者。
  3. 筛选互关having count(distinct follower_id) = 2 表示 A 和 B 都各自关注了对方,即互相关注。

这种方案只需要一次分组聚合,避免了自连接的 O(n²) 复杂度,在用户量大的社交场景下性能远优于自连接。

维度评分
题目难度⭐️⭐️
题目清晰度⭐️⭐️⭐️⭐️⭐️
业务常见度⭐️⭐️⭐️⭐️⭐️

三、逐步推导

步骤1:归一化用户对并统计每个方向

least/greatest 把关注关系映射为无向用户对,同时统计每对用户中"谁关注了谁"。

执行SQL

select
least(follower_id, followee_id) as user_a,
greatest(follower_id, followee_id) as user_b,
count(distinct follower_id) as direction_cnt
from t5_follow_record
group by least(follower_id, followee_id), greatest(follower_id, followee_id)
order by user_a, user_b

执行结果

+---------+---------+----------------+
| user_a | user_b | direction_cnt |
+---------+---------+----------------+
| u001 | u002 | 2 |
| u001 | u003 | 1 |
| u001 | u004 | 2 |
| u001 | u005 | 1 |
| u002 | u003 | 2 |
| u003 | u004 | 1 |
+---------+---------+----------------+
6 rows selected (1.589 seconds)(https://www.dwsql.com)

direction_cnt = 2 表示这对用户互相关注(两个方向都有),= 1 表示只有单向关注。

步骤2:筛选互相关注的用户对

having count(distinct follower_id) = 2 只保留互关对。

执行SQL

select
least(follower_id, followee_id) as user_a,
greatest(follower_id, followee_id) as user_b
from t5_follow_record
group by least(follower_id, followee_id), greatest(follower_id, followee_id)
having count(distinct follower_id) = 2
order by user_a, user_b

执行结果

+---------+---------+
| user_a | user_b |
+---------+---------+
| u001 | u002 |
| u001 | u004 |
| u002 | u003 |
+---------+---------+
3 rows selected (0.612 seconds)(https://www.dwsql.com)

结果:共有3对互关用户:(u001, u002)、(u001, u004)、(u002, u003)。

步骤3:(可选) 显示互关双方各自关注的时间

用条件聚合 max(case when ...) 分别取出两个方向的关注时间,判断谁先关注。

执行SQL

select
least(follower_id, followee_id) as user_a,
greatest(follower_id, followee_id) as user_b,
max(case when follower_id < followee_id then follow_time end) as a_follow_b_at,
max(case when follower_id > followee_id then follow_time end) as b_follow_a_at
from t5_follow_record
group by least(follower_id, followee_id), greatest(follower_id, followee_id)
having count(distinct follower_id) = 2
order by user_a, user_b

执行结果

+---------+---------+----------------------+----------------------+
| user_a | user_b | a_follow_b_at | b_follow_a_at |
+---------+---------+----------------------+----------------------+
| u001 | u002 | 2024-06-01 08:00:00 | 2024-06-02 09:00:00 |
| u001 | u004 | 2024-06-05 14:00:00 | 2024-06-01 13:00:00 |
| u002 | u003 | 2024-06-04 12:00:00 | 2024-06-03 11:00:00 |
+---------+---------+----------------------+----------------------+
3 rows selected (0.967 seconds)(https://www.dwsql.com)

a_follow_b_at 是 user_a 关注 user_b 的时间,b_follow_a_at 是 user_b 关注 user_a 的时间。如 u001 在 6-01 先关注了 u002,u002 在 6-02 回关。

四、常见坑点

坑1:归一化时不加 greatest 会导致重复

只用 least(follower_id, followee_id) 分组而忘了 greatest,或反过来,都会把 (A,B) 和 (B,A) 拆成两组。必须同时用 leastgreatest 组合成唯一键,确保有向关系正确归一化到同一个无向对。

坑2:判断双向用 count(distinct follower_id) 而非 count(*)

如果数据存在重复插入,同一方向可能出现多行,count(*) = 2 会被重复行干扰。用 count(distinct follower_id) 更稳健——它统计的是"有几个不同的用户在这个对里主动关注过对方",等于 2 才代表双向。

坑3:归一化前的 ID 清洗

如果用户ID在录入时有前后空格或大小写不一致(如 'U001' vs 'u001'),least/greatest 会把它们当成不同用户。生产环境应在聚合前对 ID 做 trim()upper() 处理。

五、知识点总结

考点说明
least / greatest 归一化把有向关系映射为无向用户对,作为分组唯一键
group by + having按用户对分组,having count(distinct) = 2 筛选双向关注
count(distinct follower_id)统计每个用户对里主动关注方个数,判断是否双向
条件聚合 max(case when)分别取出两个方向的关注时间,避免自连接

六、建表语句和数据插入

点击展开 DDL & DML
--建表语句
create table if not exists t5_follow_record (
follower_id string comment '关注者ID',
followee_id string comment '被关注者ID',
follow_time string comment '关注时间'
);

--数据插入
insert into t5_follow_record(follower_id, followee_id, follow_time) values
('u001', 'u002', '2024-06-01 08:00:00'),
('u002', 'u001', '2024-06-02 09:00:00'),
('u001', 'u003', '2024-06-01 10:00:00'),
('u003', 'u002', '2024-06-03 11:00:00'),
('u002', 'u003', '2024-06-04 12:00:00'),
('u004', 'u001', '2024-06-01 13:00:00'),
('u001', 'u004', '2024-06-05 14:00:00'),
('u005', 'u001', '2024-06-06 15:00:00'),
('u003', 'u004', '2024-06-02 16:00:00');
📱关注公众号

「数据仓库技术」文章同步更新,不错过每一篇干货

微信公众号二维码
💬加群交流

备注「数据仓库技术」加入社群,每日一道大厂SQL真题

交流微信二维码

你可能还想看