

这道题来自腾讯微信/QQ的社交图谱数据分析岗面试。社交关系链是腾讯最核心的数据资产——"好友的好友"(二度好友)是好友推荐、社交广告投放和社区发现的基础。分析每个用户的二度好友规模可以量化其"社交拓展潜力"。
业务场景:微信的"你可能认识的人"推荐列表,核心逻辑就是"好友的好友"。如果A和B有很多共同好友,但A和B还不是好友,那么系统就会把B推荐给A。二度好友关系链分析就是这个推荐逻辑的第一步。
现有一张用户好友关系表 t14_user_friend,记录了用户之间的好友关系(好友关系是双向的,表中每条记录都表示一条单向关系,但实际好友关系两边都有记录)。请分析每个用户的二度好友(好友的好友)数量,即通过一个中间好友可以触达、但尚未成为直接好友的用户数。
好友关系表 t14_user_friend:
+----------+------------+
| user_id | friend_id |
+----------+------------+
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 3 |
| 2 | 4 |
| 3 | 1 |
| 3 | 2 |
| 3 | 5 |
| 4 | 2 |
| 4 | 5 |
| 4 | 6 |
| 5 | 3 |
| 5 | 4 |
| 5 | 6 |
| 6 | 4 |
| 6 | 5 |
+----------+------------+
二度好友即"好友的好友",在社交网络中常用于"可能认识的人"推荐。核心分析思路:
t2.friend_id != t1.user_id 防止自己出现在二度好友结果中;LEFT JOIN ... WHERE IS NULL 排除已是直接好友的用户,比 NOT IN 更安全(避免 NULL 陷阱);COUNT(DISTINCT) 去重后计算每个用户的二度好友数量;维度 | 评分 |
|---|---|
题目难度 | ⭐️⭐️⭐️⭐️ |
题目清晰度 | ⭐️⭐️⭐️⭐️ |
业务常见度 | ⭐️⭐️⭐️⭐️⭐️ |
执行SQL
-- 以好友为桥梁进行自连接
select t1.user_id, -- 目标用户
t1.friend_id, -- 直接好友(中间桥梁)
t2.friend_id as friend_of_friend -- 好友的好友
from t14_user_friend t1
join t14_user_friend t2
on t1.friend_id = t2.user_id -- t1的好友 = t2的用户
执行结果(部分)
+----------+------------+-------------------+
| user_id | friend_id | friend_of_friend |
+----------+------------+-------------------+
| 1 | 2 | 4 |
| 1 | 2 | 3 |
| 1 | 2 | 1 |
| 1 | 3 | 5 |
| 1 | 3 | 2 |
| 1 | 3 | 1 |
| 2 | 1 | 3 |
| 2 | 1 | 2 |
| 2 | 3 | 5 |
| 2 | 3 | 2 |
| 2 | 3 | 1 |
| 2 | 4 | 6 |
| 2 | 4 | 5 |
| 2 | 4 | 2 |
| 3 | 1 | 3 |
| 3 | 1 | 2 |
| 3 | 2 | 4 |
| 3 | 2 | 3 |
| 3 | 2 | 1 |
| 3 | 5 | 6 |
| 3 | 5 | 4 |
| 3 | 5 | 3 |
| 4 | 2 | 4 |
| 4 | 2 | 3 |
| 4 | 2 | 1 |
| 4 | 5 | 6 |
| 4 | 5 | 4 |
| 4 | 5 | 3 |
| 4 | 6 | 5 |
| 4 | 6 | 4 |
| 5 | 3 | 5 |
| 5 | 3 | 2 |
| 5 | 3 | 1 |
| 5 | 4 | 6 |
| 5 | 4 | 5 |
| 5 | 4 | 2 |
| 5 | 6 | 5 |
| 5 | 6 | 4 |
| 6 | 4 | 6 |
| 6 | 4 | 5 |
| 6 | 4 | 2 |
| 6 | 5 | 6 |
| 6 | 5 | 4 |
| 6 | 5 | 3 |
+----------+------------+-------------------+
44 rows selected (0.449 seconds)(https://www.dwsql.com)
步骤1的结果中包含了两类"噪音":friend_of_friend = user_id(用户自身)和 friend_of_friend 已经是直接好友的。通过 LEFT JOIN 原好友关系表,用 WHERE t3.user_id IS NULL 排除这两类数据,得到纯净的二度好友明细。同时保留 t1.friend_id 作为中间桥梁好友,便于追溯推荐来源。
执行SQL
select t1.user_id,
t1.friend_id as through_friend,
t2.friend_id as second_degree_friend
from t14_user_friend t1
join t14_user_friend t2
on t1.friend_id = t2.user_id -- 好友的好友
and t2.friend_id != t1.user_id -- 排除自身
left join t14_user_friend t3
on t1.user_id = t3.user_id
and t2.friend_id = t3.friend_id -- 排除已是直接好友的
where t3.user_id is null
order by t1.user_id, t2.friend_id
执行结果
+----------+-----------------+-----------------------+
| user_id | through_friend | second_degree_friend |
+----------+-----------------+-----------------------+
| 1 | 2 | 4 |
| 1 | 3 | 5 |
| 2 | 3 | 5 |
| 2 | 4 | 5 |
| 2 | 4 | 6 |
| 3 | 2 | 4 |
| 3 | 5 | 4 |
| 3 | 5 | 6 |
| 4 | 2 | 1 |
| 4 | 2 | 3 |
| 4 | 5 | 3 |
| 5 | 3 | 1 |
| 5 | 3 | 2 |
| 5 | 4 | 2 |
| 6 | 4 | 2 |
| 6 | 5 | 3 |
+----------+-----------------+-----------------------+
16 rows selected (10.362 seconds)(https://www.dwsql.com)
以用户1为例:通过好友2可触达二度好友4,通过好友3可触达二度好友5。注意用户2本身也是用户1的直接好友,但在步骤1中 (1,2,3) 这条记录(friend_of_friend=3 已是直接好友)已被 LEFT JOIN 排除。
在步骤2明细的基础上,按 user_id 分组,用 COUNT(DISTINCT second_degree_friend) 统计每个用户的二度好友数。
执行SQL
select t1.user_id,
count(distinct t2.friend_id) as second_degree_friend_cnt
from t14_user_friend t1
join t14_user_friend t2
on t1.friend_id = t2.user_id
and t2.friend_id != t1.user_id
leftjoin t14_user_friend t3
on t1.user_id = t3.user_id
and t2.friend_id = t3.friend_id
where t3.user_id isnull
groupby t1.user_id
orderby t1.user_id
执行结果
+----------+---------------------------+
| user_id | second_degree_friend_cnt |
+----------+---------------------------+
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
| 6 | 2 |
+----------+---------------------------+
6 rows selected (1.305 seconds)(https://www.dwsql.com)
用户1和用户3的二度好友最多(各2个),用户4和5最少(各1个)。二度好友越多,社交拓展潜力越大。
坑1:自连接的数据膨胀
好友表有N条记录,自连接产生N×N行。如果好友表有1亿条记录,自连接会产生天文数字。实践中需要先筛选目标用户再加 LIMIT 或使用图数据库。
坑2:NOT IN 子查询的 NULL 陷阱
常见的排除已有好友的写法是 t2.friend_id NOT IN (SELECT friend_id FROM ...),但如果子查询结果中包含 NULL,NOT IN 会返回空集(因为 x NOT IN (1, 2, NULL) 等价于 x <> 1 AND x <> 2 AND x <> NULL,而任何值与 NULL 比较都为 UNKNOWN)。推荐用 LEFT JOIN ... WHERE IS NULL 替代,逻辑清晰且无 NULL 风险。
坑3:DISTINCT 去重的必要性
通过不同共同好友可能找到相同的二度好友(如用户4既是好友2的好友,也是好友3的好友),需用 COUNT(DISTINCT t2.friend_id) 去重得到正确数量。
坑4:好友关系单向 vs 双向的处理
如果好友表只存了单向关系(A→B),需要 UNION ALL 反向复制确保双向查询都成立。本题假设已双向存储。
好友表 JOIN 自身,以好友为桥梁找到二度好友(好友的好友)CREATE TABLE t14_user_friend (
user_id bigintCOMMENT'用户ID',
friend_id bigintCOMMENT'好友ID'
) COMMENT'用户好友关系表';
-- 数据插入
INSERTINTO t14_user_friend VALUES
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(2, 4),
(3, 1),
(3, 2),
(3, 5),
(4, 2),
(4, 5),
(4, 6),
(5, 3),
(5, 4),
(5, 6),
(6, 4),
(6, 5);