COUNT(DISTINCT) 是共同好友统计的起点,因直接 COUNT(*) 会因重复关系记录导致同一好友被多次计数,必须用 COUNT(DISTINCT friend_id) 确保每人只计一次;正确做法是先分别获取两人好友集合再求交集,常用 INNER JOIN 或 EXISTS 实现,并依赖 (user_id, friend_id) 联合索引保障性能。

为什么 COUNT(DISTINCT) 是共同好友统计的起点
直接用 COUNT(*) 会重复计数——如果 A 和 B 多次出现在同一张关系表里(比如双向关注、多次互动记录),就会把同一个人算成多个“共同好友”。必须用 COUNT(DISTINCT user_id) 才能确保每个好友只算一次。
常见错误是先 GROUP BY 再去重,结果发现人数偏少。真正该做的是:先找出两人各自的好友集合,再取交集,最后计数。数据库不支持直接写“集合交集”,所以得靠 INNER JOIN 或子查询模拟。
- 场景:好友关系存在
friends表中,字段为user_id和friend_id - 目标:统计用户 101 和用户 205 的共同好友数量
- 别用
WHERE user_id IN (101, 205)然后COUNT(DISTINCT friend_id)——这算的是“他们俩加起来的好友数”,不是“重叠部分”
用 INNER JOIN 实现好友交集的可靠写法
这是最直观、兼容性最好的方式,MySQL、PostgreSQL、SQL Server 都能跑通,且执行计划通常可优化。
SELECT COUNT(DISTINCT f1.friend_id) AS common_friends_count FROM friends f1 INNER JOIN friends f2 ON f1.friend_id = f2.friend_id WHERE f1.user_id = 101 AND f2.user_id = 205;
关键点:
-
f1.friend_id = f2.friend_id是交集逻辑的核心,不是连接两个用户本身 - WHERE 条件必须分别限定
f1.user_id和f2.user_id,不能写成WHERE f1.user_id IN (101, 205) AND f2.user_id IN (101, 205)——会引入冗余匹配 - 如果表没在
(user_id, friend_id)上建联合索引,这个查询可能全表扫描,慢得明显
用 EXISTS 替代 JOIN 的适用场景
当一方好友量极大(比如大 V 有 500 万粉丝),而另一方只有几百人时,EXISTS 往往比 JOIN 更快,因为可以提前终止子查询。
SELECT COUNT(*) AS common_friends_count
FROM friends f1
WHERE f1.user_id = 101
AND EXISTS (
SELECT 1 FROM friends f2
WHERE f2.user_id = 205 AND f2.friend_id = f1.friend_id
);
注意:
- 这里用
COUNT(*)就够了,因为外层每行对应一个确定的f1.friend_id,不存在重复 - 子查询里必须用
f2.friend_id = f1.friend_id,不能反过来写成f1.friend_id = f2.friend_id(语义一样,但某些旧版 MySQL 优化器识别不到索引) - 如果
friends表缺乏user_id单列索引或(user_id, friend_id)联合索引,EXISTS也救不了性能
避免 COUNT(DISTINCT) 在大数据量下的隐性开销
当共同好友可能上万时,COUNT(DISTINCT) 会触发临时哈希表或排序,内存占用陡增,甚至落盘。这时候宁可分两步:先取交集 ID 列表,再计数。
- 用
SELECT DISTINCT friend_id ... INTO TEMP TABLE(PostgreSQL/SQL Server)或CREATE TEMPORARY TABLE(MySQL)缓存中间结果 - 对临时表加主键或唯一索引,后续 COUNT 就是 O(1) 或轻量扫描
- 如果业务允许误差,可考虑 HyperLogLog 类似估算(如 PostgreSQL 的
HLL扩展),但社交场景通常要求精确值
最常被忽略的是索引设计:(user_id, friend_id) 必须是联合索引,且顺序不能颠倒——否则 WHERE user_id = ? 无法走索引,整个查询就退化成扫表。

















