单张friends表存user_id+friend_id会踩三个坑:关系方向模糊、重复插入难防、双向操作变复杂;应改用friendships表,含user_id、target_id、status等字段,并加联合唯一索引和覆盖索引优化查询。

为什么不能用单张表存 user_id + friend_id 就完事?
直接建 friends 表,字段为 user_id 和 friend_id,看起来简洁,但实际会踩三个坑:关系方向模糊、重复插入难防、双向操作变复杂。比如 A 加了 B 为好友,数据库里存了 (1, 2),那 B 是否也自动关注了 A?业务上通常要区分“我关注的人”和“关注我的人”,甚至还要支持“互相关注但状态不同”(如一方已验证、另一方待确认)。单向记录无法支撑这类扩展。
推荐结构:一张关系表 + 明确状态字段
用 friendships 表,至少包含:id、user_id、target_id、status、created_at。其中:status 推荐用 tinyint 或 enum,值设为 0=pending、1=accepted、2=rejected、3=blocked。关键点:
-
user_id是发起方,target_id是被操作用户,方向明确,查“A的关注列表”就是WHERE user_id = A,查“A的粉丝”就是WHERE target_id = A - 联合唯一索引必须加:
UNIQUE KEY `ux_user_target` (`user_id`, `target_id`),防止重复申请 - 如果要支持“双向好友”逻辑(即只有双方都
accepted才算真正好友),不要在写入时硬编码双条记录,而是在查询层或定时任务中判断WHERE (user_id = A AND target_id = B AND status = 1) AND (user_id = B AND target_id = A AND status = 1)
怎么高效查“共同好友”?别用 JOIN 套子套子
查用户 A 和用户 B 的共同好友,常见错误是写两层子查询或 INNER JOIN 两张 friendships 表——数据量一上去就慢。更稳的做法是用 IN + 覆盖索引:
SELECT target_id
FROM friendships
WHERE user_id = 123
AND status = 1
AND target_id IN (
SELECT target_id
FROM friendships
WHERE user_id = 456 AND status = 1
);
前提是 (user_id, status, target_id) 有联合索引,这样内层子查询能走索引下推,外层也能快速定位。如果并发高、实时性要求不强,共同好友结果完全可以缓存到 Redis,键名如 common_friends:123:456,过期时间设为 1 小时。
删除好友时,到底该删一条还是两条记录?
答案是:只删一条,且必须明确删哪条。用户 A 主动取消关注 B,只需执行:DELETE FROM friendships WHERE user_id = A AND target_id = B。不要连带删 (B, A) 那条——因为那是 B 对 A 的独立关系,可能仍是“B 关注 A”或“B 拉黑 A”。误删会导致状态丢失。补充建议:
- 加软删字段
is_deleted TINYINT DEFAULT 0,配合定时归档,比物理删除更安全 - 所有删除操作必须走事务,并同步更新内存缓存(如用户 A 的关注数减 1)
- 如果业务允许“单向取关不通知”,那删完就结束;如果要通知对方(如“XX 取关了你”),得额外发消息,不能依赖数据库触发器——MySQL 触发器没法可靠调外部服务
status 字段组合判断,而不是靠表结构本身。字段设计要留出至少 1–2 个备用状态位。










