为什么不能用单张表存 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 个备用状态位。
