首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >SQL 全量好友推荐:自连接 + 排除已有好友 + ROW_NUMBER 取 Top N(腾讯面试题)

SQL 全量好友推荐:自连接 + 排除已有好友 + ROW_NUMBER 取 Top N(腾讯面试题)

作者头像
数据仓库晨曦
发布2026-07-16 21:21:08
发布2026-07-16 21:21:08
00
举报
文章被收录于专栏:数据仓库技术数据仓库技术

一、题目背景

这道题来自腾讯微信事业群的数据开发岗面试。"你可能认识的人"是微信好友推荐的核心功能,背后的算法基础就是共同好友计算——两个非好友用户的共同好友越多,他们现实中认识的概率越大。这道题考察的是如何用一条 SQL 为所有用户批量生成推荐列表,而非只给某一个用户推荐。

业务场景:微信的"朋友推荐"模块每天需要为全量用户更新推荐列表,不能逐用户查一次(那样要上亿次查询)。生产环境通过离线 T+1 批处理,用一条 SQL 为所有用户同时计算出 Top N 推荐好友,写入 Redis 供在线读取。这道题的 SQL 就是该批处理任务的核心逻辑。

二、题目

现有用户好友关系表 t7_user_friend,包含用户ID及其好友ID,好友关系为双向关注(若A是B的好友,则B也是A的好友)。请为每一位用户推荐潜在好友(非已有好友),按共同好友数量降序排列,取 Top 1。

好友关系表 t7_user_friend:

代码语言:javascript
复制
+----------+------------+
| user_id  | friend_id  |
+----------+------------+
| 1        | 2          |
| 1        | 3          |
| 1        | 4          |
| 2        | 1          |
| 2        | 3          |
| 2        | 5          |
| 3        | 1          |
| 3        | 2          |
| 3        | 4          |
| 3        | 5          |
| 4        | 1          |
| 4        | 3          |
| 4        | 5          |
| 5        | 2          |
| 5        | 3          |
| 5        | 4          |
+----------+------------+

三、思路分析

这是典型的多用户共同好友推荐题型,核心是 自连接找共同好友 + LEFT JOIN 排除已有好友 + ROW_NUMBER 分组取 Top N

  1. 自连接找共同好友:以好友ID为桥梁 t1.friend_id = t2.friend_id,自连接找出任意两个用户(user_a, user_b)共享的好友。t1.user_id != t2.user_id 排除自己和自己的配对
  2. 排除已有好友:用 LEFT JOIN ... WHERE IS NULL 排除已经是直接好友的用户对,只保留陌生人推荐
  3. 分组计数 + 排名GROUP BY 统计共同好友数,ROW_NUMBER() OVER (PARTITION BY user_a ORDER BY common_friend_cnt DESC) 为每个用户取 Top N

维度

评分

题目难度

⭐️⭐️⭐️⭐️

题目清晰度

⭐️⭐️⭐️⭐️

业务常见度

⭐️⭐️⭐️⭐️⭐️

四、逐步推导

步骤1:自连接找出所有用户对的共同好友

将好友关系表自连接,连接条件为 t1.friend_id = t2.friend_id(同一个好友作为桥梁),找到每对用户 (user_a, user_b) 之间的共同好友。t1.user_id != t2.user_id 排除自己和自己的无意义配对。

执行SQL

代码语言:javascript
复制
select t1.user_id   as user_a,
       t2.user_id   as user_b,
       t1.friend_id as common_friend
from t7_user_friend t1
         join t7_user_friend t2
              on t1.friend_id = t2.friend_id
where t1.user_id != t2.user_id
order by t1.user_id, t2.user_id

执行结果

代码语言:javascript
复制
+---------+---------+----------------+
| user_a  | user_b  | common_friend  |
+---------+---------+----------------+
| 1       | 2       | 3              |
| 1       | 3       | 2              |
| 1       | 3       | 4              |
| 1       | 4       | 3              |
| 1       | 5       | 2              |
| 1       | 5       | 3              |
| 1       | 5       | 4              |
| 2       | 1       | 3              |
| 2       | 3       | 1              |
| 2       | 3       | 5              |
| 2       | 4       | 1              |
| 2       | 4       | 3              |
| 2       | 4       | 5              |
| 2       | 5       | 3              |
| 3       | 1       | 2              |
| 3       | 1       | 4              |
| 3       | 2       | 1              |
| 3       | 2       | 5              |
| 3       | 4       | 1              |
| 3       | 4       | 5              |
| 3       | 5       | 2              |
| 3       | 5       | 4              |
| 4       | 1       | 3              |
| 4       | 2       | 1              |
| 4       | 2       | 3              |
| 4       | 2       | 5              |
| 4       | 3       | 1              |
| 4       | 3       | 5              |
| 4       | 5       | 3              |
| 5       | 1       | 2              |
| 5       | 1       | 3              |
| 5       | 1       | 4              |
| 5       | 2       | 3              |
| 5       | 3       | 2              |
| 5       | 3       | 4              |
| 5       | 4       | 3              |
+---------+---------+----------------+
36 rows selected (1.073 seconds)(https://www.dwsql.com)

以 user_a=1 为例,common_friend 为 2,3,4,说明用户1通过好友2,3,4分别连接到了其他用户。其中 (1,5) 出现了3次(好友2,3,4都是共同好友),说明1和5的共同好友最多。

步骤2:排除已有好友并统计共同好友数

步骤1中包含了已经是好友的用户对(如 user_a=1, user_b=2,他们本来就是好友)。通过 LEFT JOIN 原好友关系表,用 WHERE f.user_id IS NULL 排除直接好友,只保留陌生人对。然后 GROUP BY + COUNT(DISTINCT) 统计每对用户的共同好友数。

执行SQL

代码语言:javascript
复制
select t1.user_id                         as user_a,
       t2.user_id                         as user_b,
       count(distinct t1.friend_id)       as common_friend_cnt
from t7_user_friend t1
         join t7_user_friend t2
              on t1.friend_id = t2.friend_id
         left join t7_user_friend f
                   on t1.user_id = f.user_id
                       and t2.user_id = f.friend_id
where t1.user_id != t2.user_id
  and f.user_id is null
group by t1.user_id, t2.user_id
order by t1.user_id, common_friend_cnt desc

执行结果

代码语言:javascript
复制
+---------+---------+--------------------+
| user_a  | user_b  | common_friend_cnt  |
+---------+---------+--------------------+
| 1       | 5       | 3                  |
| 2       | 4       | 3                  |
| 4       | 2       | 3                  |
| 5       | 1       | 3                  |
+---------+---------+--------------------+
4 rows selected (1.254 seconds)(https://www.dwsql.com)

用户3没有出现在 user_a 列中,因为用户3的好友已经覆盖了所有其他用户(1,2,4,5),不存在可推荐的陌生人。其他用户均只有1个陌生人,且共同好友数都是3。

步骤3:用 ROW_NUMBER 为每个用户取 Top N 推荐

在步骤2的基础上,用 ROW_NUMBER() OVER (PARTITION BY user_a ORDER BY common_friend_cnt DESC) 为每个用户的候选推荐排名,外层取 rn = 1 得到最佳推荐。

执行SQL

代码语言:javascript
复制
select user_a, user_b as recommend_user, common_friend_cnt
from (
    select t1.user_id                         as user_a,
           t2.user_id                         as user_b,
           count(distinct t1.friend_id)       as common_friend_cnt,
           row_number() over (partition by t1.user_id
                              order by count(distinct t1.friend_id) desc) as rn
    from t7_user_friend t1
             join t7_user_friend t2
                  on t1.friend_id = t2.friend_id
             left join t7_user_friend f
                       on t1.user_id = f.user_id
                           and t2.user_id = f.friend_id
    where t1.user_id != t2.user_id
      and f.user_id is null
    group by t1.user_id, t2.user_id
) t
where rn = 1
order by user_a

执行结果

代码语言:javascript
复制
+---------+-----------------+--------------------+
| user_a  | recommend_user  | common_friend_cnt  |
+---------+-----------------+--------------------+
| 1       | 5               | 3                  |
| 2       | 4               | 3                  |
| 4       | 2               | 3                  |
| 5       | 1               | 3                  |
+---------+-----------------+--------------------+
4 rows selected (1.025 seconds)(https://www.dwsql.com)

全量推荐结果:用户1推荐5,用户2推荐4,用户4推荐2,用户5推荐1。用户3无陌生人可推荐(不出现在结果中)。在实际业务中,用户3的推荐位可回退为二度好友或其他推荐策略。

五、常见坑点

坑1:用 NOT IN 排除已有好友

NOT IN (SELECT ...) 写法虽然直观,但有两大隐患:一是子查询每行都执行,数据量大时性能差;二是子查询结果中若包含 NULL,NOT IN 会返回空集(因为 x NOT IN (1, 2, NULL) 等价于 x <> 1 AND x <> 2 AND x <> NULL,而 x <> NULL 永远为 UNKNOWN)。推荐用 LEFT JOIN ... WHERE IS NULL 代替。

坑2:忘记去重 COUNT(DISTINCT)

自连接时,如果同一对用户有多条共同好友记录(如步骤1中 (1,5) 出现3次),直接 COUNT(*) 会算多次。但共同好友的计算逻辑是"有多少个共同好友",不是"共同好友关系在表里出现了几次",必须用 COUNT(DISTINCT t1.friend_id)

坑3:ROW_NUMBER 在 GROUP BY 后的作用域

ROW_NUMBER() OVER (...) 中可以使用聚合函数的结果(如 ORDER BY count(distinct ...) DESC),窗口函数在 GROUP BY 之后执行,可以正确引用聚合后的列。但注意不能直接在 PARTITION BYORDER BY 里用别名,需要重复写完整的聚合表达式。

坑4:用户3这类"无陌生人"的边界情况

当某用户的好友覆盖了所有其他用户时(全连接),LEFT JOIN 排除后该用户在 user_a 列为空。结果集中不会出现该用户,业务侧需要对这类用户做降级推荐(如二度好友、热门用户等),否则前端展示空白。

六、举一反三

  1. 取 Top N 而非 Top 1:将 WHERE rn = 1 改为 WHERE rn <= 3 即可为每个用户推荐最多3个好友,适合"你可能认识的人"列表场景
  2. 最小共同好友数阈值:在步骤2的 HAVING 或步骤3的外层加上 WHERE common_friend_cnt >= 2,过滤共同好友太少的低质量推荐
  3. 双向高相关推荐:在步骤3的结果上,JOIN 反向推荐表,筛选出"A推荐B"且"B也推荐A"的相互推荐对,这类推荐的可信度更高
  4. 加权共同好友:如果好友关系表有时间戳或互动次数字段,可给近期/高频互动的好友更高权重,用加权计数替代 COUNT(DISTINCT)

七、知识点总结

代码语言:javascript
复制
同一张表以好友ID为桥梁 JOIN 自身,找出用户对之间的共同好友

八、建表语句和数据插入

代码语言:javascript
复制
CREATE TABLE t7_user_friend (
    user_id   int COMMENT '用户ID',
    friend_id int COMMENT '好友ID'
) COMMENT '用户好友关系表';

INSERT INTO t7_user_friend VALUES
(1,2),(1,3),(1,4),
(2,1),(2,3),(2,5),
(3,1),(3,2),(3,4),(3,5),
(4,1),(4,3),(4,5),
(5,2),(5,3),(5,4);
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据仓库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、题目背景
  • 二、题目
  • 三、思路分析
  • 四、逐步推导
    • 步骤1:自连接找出所有用户对的共同好友
    • 步骤2:排除已有好友并统计共同好友数
    • 步骤3:用 ROW_NUMBER 为每个用户取 Top N 推荐
  • 五、常见坑点
  • 六、举一反三
  • 七、知识点总结
  • 八、建表语句和数据插入
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档