快手大数据面试SQL-关注关系:互相关注的用户对
⚠️ 待修正
一、题目背景
这道题来自快手社交数据团队的数据分析岗面试。快手作为短视频+社交平台,用户之间的关注关系构成了社交图谱的基础。互相关注(俗称"互关")是两个用户之间最强的关系信号——它不仅表示单向兴趣,更代表双向互动和真实社交连接。
业务场景:快手的"可能认识的人"推荐模块需要基于互关关系做二度好友推荐。同时直播团队的"好友在看"功能、私信功能的权限控制也都依赖互关关系的判定。找出所有互关用户对是这些下游业务的基础数据。
二、题目
已知有表t1_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 |
+-------------+-------------+----------------------+
三、思路分析
本题是社交网络关系分析中的经典问题。核心考察的是INNER JOIN自连接的理解。
核心思路:
- 自连接匹配反向关系:将t1_follow_record与自身做INNER JOIN,连接条件为 A.follower_id = B.followee_id AND A.followee_id = B.follower_id。这样只有双向关注的关系才会匹配上。
- 去重:自连接会产生"A关注B且B关注A"和"B关注A且A关注B"两条记录,需要只保留一条。可以通过条件
A.follower_id < A.followee_id来只保留较小ID在前的那条。 - 进阶:如果需要统计互关对数、互关用户数、或计算某个用户的互关好友列表,都可以基于此逻辑扩展。
在快手等社交平台中,互相关注(互关)是衡量用户社交关系的强信号,常用于好友推荐、亲密关系分析。
| 维度 | 评分 |
|---|---|
| 题目难度 | ⭐️⭐️ |
| 题目清晰度 | ⭐️⭐️⭐️⭐️⭐️ |
| 业务常见度 | ⭐️⭐️⭐️⭐️⭐️ |
四、逐步推导
步骤1:自连接找出所有双向关注记录
用INNER JOIN连接关注表的两个副本,找出双向关注的用户对。注意这会返回每对互关关系的两条记录(AB和BA)。
执行SQL
select
a.follower_id as user_a,
a.followee_id as user_b,
a.follow_time as a_follow_b_time,
b.follow_time as b_follow_a_time
from t1_follow_record a
inner join t1_follow_record b
on a.follower_id = b.followee_id
and a.followee_id = b.follower_id
执行结果
+--------+--------+----------------------+----------------------+
| user_a | user_b | a_follow_b_time | b_follow_a_time |
+--------+--------+----------------------+----------------------+
| u001 | u002 | 2024-06-01 08:00:00 | 2024-06-02 09:00:00 |
| u002 | u001 | 2024-06-02 09:00:00 | 2024-06-01 08:00:00 |
| u001 | u004 | 2024-06-05 14:00:00 | 2024-06-01 13:00:00 |
| u004 | u001 | 2024-06-01 13:00:00 | 2024-06-05 14:00:00 |
| u002 | u003 | 2024-06-04 12:00:00 | 2024-06-03 11:00:00 |
| u003 | u002 | 2024-06-03 11:00:00 | 2024-06-04 12:00:00 |
+--------+--------+----------------------+----------------------+
步骤2:去重,每对只保留一条记录
使用条件 a.follower_id < a.followee_id(或使用子查询 + DISTINCT)。由于自连接会产生对称的重复行,取id较小的作为user_a即可保证每对互关关系只出现一次。
执行SQL
select distinct
a.follower_id as user_a,
a.followee_id as user_b
from t1_follow_record a
inner join t1_follow_record b
on a.follower_id = b.followee_id
and a.followee_id = b.follower_id
where a.follower_id < a.followee_id
执行结果
+--------+--------+
| user_a | user_b |
+--------+--------+
| u001 | u002 |
| u001 | u004 |
| u002 | u003 |
+--------+--------+
结果:共有3对互关用户:(u001, u002), (u001, u004), (u002, u003)。
步骤3:(可选) 显示互关时间详情
如果想了解互关双方各自是什么时候关注对方的,保留关注时间信息。
执行SQL
select
a.follower_id as user_a,
a.followee_id as user_b,
a.follow_time as a_followed_b_at,
b.follow_time as b_followed_a_at,
case
when a.follow_time < b.follow_time then concat(user_a, '先关注')
when a.follow_time > b.follow_time then concat(user_b, '先关注')
else '同时互关'
end as remark
from t1_follow_record a
inner join t1_follow_record b
on a.follower_id = b.followee_id
and a.followee_id = b.follower_id
where a.follower_id < a.followee_id
执行结果
+--------+--------+----------------------+----------------------+----------------+
| user_a | user_b | a_followed_b_at | b_followed_a_at | remark |
+--------+--------+----------------------+----------------------+----------------+
| u001 | u002 | 2024-06-01 08:00:00 | 2024-06-02 09:00:00 | u001先关注 |
| u001 | u004 | 2024-06-05 14:00:00 | 2024-06-01 13:00:00 | u004先关注 |
| u002 | u003 | 2024-06-04 12:00:00 | 2024-06-03 11:00:00 | u003先关注 |
+--------+--------+----------------------+----------------------+----------------+
五、常见坑点
坑1:忘记去重,返回对称重复行
自连接会将(A→B)和(B→A)两条正向记录分别作为主表的行进行匹配,结果中每对互关关系会出现两次。如果直接用于下游统计(如互关用户数),会导致重复计数。必须通过 a.follower_id < a.followee_id 或使用子查询+DISTINCT去重。
坑2:混淆 LEFT JOIN 和 INNER JOIN
如果用 LEFT JOIN 代替 INNER JOIN,会把所有关注关系都保留下来,单向关注的行中B表字段全部为NULL。这样无法区分"互关"和"单向关注"。必须用 INNER JOIN 确保只保留双向匹配的记录。
坑3:忽略大小写或空格导致匹配失败
如果用户ID在录入时有前后空格或大小写不一致(如 'U001' vs 'u001'),自连接的等值条件会匹配失败。在生产环境中需要在连接前对ID字段做 TRIM() 和 UPPER() 处理。
六、举一反三
-
统计每个用户的互关好友数:基于去重后的互关结果,按 user_a 和 user_b 分别展开为两行(UNION ALL),再按用户分组 COUNT,即可得到每个用户有多少互关好友,用于社交影响力评估。
-
找出"互关三角"(三人互关圈):三个用户两两互关是一个完整的小社交圈。可以在互关结果的基础上再做一次自连接:找出(A,B)互关、(B,C)互关、(A,C)互关的组合,用于社群发现。
-
计算互关转化率:统计所有关注关系中,有多少比例最终转化为了互关关系。即
互关对数 / 所有有向关注关系数,衡量平台的社交连接效率。 -
互关关系的时间窗口分析:计算从A关注B到B回关A的平均时间差(follow_back_days),分析用户的回关行为模式。间隔越短说明互动越紧密,可用于识别"强关系"好友。
七、知识点总结
| 考点 | 说明 |
|---|---|
| INNER JOIN 自连接 | 将表与自身连接,匹配反向关注关系 |
去重条件 a.id < b.id | 对同一对互关只保留ID较小者在前的一条 |
| CASE WHEN 时序判断 | 根据关注时间判断谁先关注,用于分析关注方向 |
| DISTINCT vs GROUP BY | 在去重场景下两种方式等价,DISTINCT更简洁 |
八、建表语句和数据插入
点击展开 DDL & DML
--建表语句
CREATE TABLE IF NOT EXISTS t1_follow_record (
follower_id STRING COMMENT '关注者ID',
followee_id STRING COMMENT '被关注者ID',
follow_time STRING COMMENT '关注时间'
);
--数据插入
INSERT INTO t1_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真题
