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 |
+--------------+--------------+----------------------+
二、思路分析
本题是社交网络关系分析中的经典问题。核心思路是把有向的关注关系归一化为无向的用户对,再用分组聚合判断双向:
- 归一化用户对:用
least(follower_id, followee_id)和greatest(follower_id, followee_id)把每条关注关系映射成无向用户对 (较小ID, 较大ID)。无论 A 关注 B 还是 B 关注 A,都归到同一个 (A, B) 对。 - 分组统计方向数:按归一化后的用户对 GROUP BY,用
count(distinct follower_id)统计这对用户中有几个不同的关注者。 - 筛选互关:
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) 拆成两组。必须同时用 least 和 greatest 组合成唯一键,确保有向关系正确归一化到同一个无向对。
坑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真题
