一张表即可,用 user_relations 表存储“用户 a 关注用户 b”的单向关系,含 follower_id、followed_id、created_at 和 status 字段,配联合唯一索引和 followed_id 索引,避免冗余拆分、外键锁争与 json 存储。

用一张表还是两张表存关注和粉丝关系
直接用一张表就能搞定,别拆成 follows 和 followers 两张。关系本质是“用户 A 关注用户 B”,方向明确,冗余拆分只会让查询变慢、同步逻辑变重。
典型错误是看到“关注”和“粉丝”语义不同,就下意识建两个方向的表。其实粉丝数就是反向查一次 SELECT COUNT(*) FROM user_relations WHERE followed_id = ?,没必要预存。
-
user_relations表结构:id(主键)、follower_id(关注者)、followed_id(被关注者)、created_at; - 必须加联合唯一索引:
UNIQUE KEY uk_follower_followed (follower_id, followed_id),防重复关注; - 单独为
followed_id建索引,否则查“谁关注了我”会全表扫描; - 别加外键约束——高并发写入时锁竞争严重,应用层保证 ID 合法更可控。
怎么高效查“共同关注”和“互相关注”
共同关注不是靠子查询硬套,而是用自连接 + 聚合。互相关注也不是查两次再比对,而是用内连接一次到位。
例如查用户 A 和用户 B 的共同关注:
SELECT ur1.followed_id FROM user_relations ur1 INNER JOIN user_relations ur2 ON ur1.followed_id = ur2.followed_id WHERE ur1.follower_id = ? AND ur2.follower_id = ?;
互相关注(即双向关注)只需:
SELECT ur1.follower_id, ur1.followed_id FROM user_relations ur1 INNER JOIN user_relations ur2 ON ur1.follower_id = ur2.followed_id AND ur1.followed_id = ur2.follower_id;
- 确保
(follower_id, followed_id)和followed_id都有索引,否则连接性能断崖下跌; - 如果要分页,别在连接结果上直接
LIMIT,先用IN或JOIN子查询缩小范围; - “是否互关”这种高频判断,可在应用层缓存布尔值,避免每次查库。
取消关注后要不要物理删除记录
物理删除没问题,但得配软删除字段才稳妥。社交场景里,“历史关注关系”可能用于推荐、反作弊或数据回溯,一删了之容易踩坑。
- 加
status TINYINT DEFAULT 1(1=有效,0=已取消),查询时默认加WHERE status = 1; - 取消关注只改
status,不删行——这样粉丝数统计、时间线排序(按created_at)都不受影响; - 定期归档旧的
status = 0记录(比如半年前),而不是永久堆积; - 如果业务明确不需要历史,且 QPS 极高,物理删除 + 清理索引碎片更省资源,但上线前务必压测删除语句的锁表现。
MySQL 8.0+ 可以用 JSON 列存关注标签吗
不建议。JSON 字段看着灵活,但在关注关系这种强结构、高频查询的场景里,它会让索引失效、无法原子更新、排查慢查困难。
比如想查“关注了科技类博主的用户”,用 JSON 存分类就得 JSON_CONTAINS(tags, '"tech"'),没法走索引,全表扫描是常态。
- 标签维度应该独立成关联表
user_relation_tags(relation_id, tag_name),支持多值、可索引、可统计; - MySQL 的
JSON_EXTRACT函数在 WHERE 条件中基本等于放弃优化器; - 除非是极低频的运营后台导出需求,否则别把关系型数据塞进 JSON。
真正容易被忽略的是时间精度和时区——created_at 必须用 DATETIME(3) 或更高精度,否则同一秒内多次操作可能因主键冲突失败;所有时间统一用 UTC 写入,展示层再转本地时区。











