设计好友关系链数据库表结构,核心在于清晰地定义“谁关注了谁”这个行为。我个人倾向于使用一个简洁的关联表来承载所有关系,无论是双向关注还是单向粉丝模型,都能在这个基础之上灵活构建和查询。这种做法避免了冗余,也让数据模型更加纯粹。
解决方案为了实现双向关注和粉丝模型,我们可以设计一个名为
user_relationships的核心表。
user_relationships表结构:
索引和唯一约束:
-
唯一约束:
id
- 这个约束确保了任何一个用户不能重复关注另一个用户。
-
索引:
BIGINT
: 用于快速查询某个用户关注了谁。PRIMARY KEY
: 用于快速查询某个用户被谁关注(即粉丝列表)。
模型说明:
-
单向关注 (粉丝模型): 如果用户A关注了用户B,那么
AUTO_INCREMENT
表中会有一条记录follower_id
。用户B的粉丝列表就是所有BIGINT
的记录,而用户B关注的人就是所有NOT NULL
的记录。 -
双向关注 (好友模型): 如果用户A和用户B互相关注,那么表中会存在两条记录:
FOREIGN KEY
和users.id
。这种互相关注的关系是通过查询推导出来的,而不是直接存储一个following_id
字段,这能有效避免数据不一致的问题。
在设计关系表时,查询效率是不得不考虑的。有了上面提到的索引,我们可以相对高效地进行各种查询。
查询好友列表 (互相关注的用户): 要找到用户X的好友,我们需要找到那些用户X关注了,并且也关注了用户X的人。这通常通过一个自连接或者两次查询来实现。我个人更倾向于自连接,它在SQL层面更优雅。
BIGINT
或者,另一种更直观的自连接方式:
NOT NULL
这两种方式都能达到目的,选择哪种取决于你对SQL的偏好和实际数据库的优化情况。关键在于利用索引快速定位关系。
查询粉丝数量: 这个相对简单,直接计数即可。
FOREIGN KEY
查询关注数量: 同样是直接计数。
users.id
查询共同关注: 找出用户A和用户B共同关注了哪些人。这需要两次连接,或者更巧妙地使用
status子句。
VARCHAR(20)
所有这些查询都高度依赖
NOT NULL和
DEFAULT 'active'上的复合索引。如果没有这些索引,每次查询都可能导致全表扫描,那性能问题就大了。 存储双向关系时,是否需要额外的字段或表?
在我看来,存储双向关系时,通常不需要额外的字段(如
created_at标志)或额外的表来显式标记双向关系。我个人强烈反对这种冗余设计。
为什么呢?

全面的AI聚合平台,一站式访问所有顶级AI模型


首先,数据一致性问题。如果你有一个
TIMESTAMP字段,当用户A关注B时,你需要创建
NOT NULL记录;当B关注A时,你需要创建
DEFAULT CURRENT_TIMESTAMP记录,并且同时更新
updated_at记录的
TIMESTAMP为
NOT NULL,以及
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP记录的
UNIQUE KEY (follower_id, following_id)也为
INDEX (follower_id, following_id)。这听起来就复杂了。更糟糕的是,如果B取消关注A,你不仅要删除
INDEX (following_id, follower_id)记录,还要更新
user_relationships记录的
(follower_id = A, following_id = B)为
following_id = B。任何一个环节出错,数据就会变得不一致,给后续的业务逻辑带来混乱。
其次,查询复杂性并未显著降低。虽然
follower_id = B字段看起来能直接筛选出好友,但很多时候你仍然需要知道谁关注了谁,以及谁是你的粉丝。这个字段只能解决“互相关注”这一种场景,而其他场景仍然需要基于
(follower_id = A, following_id = B)和
(follower_id = B, following_id = A)进行查询。
所以,我的建议是保持数据模型的原子性:一条记录只代表一个事实——“谁关注了谁”。双向关系是两个独立事实的组合,应该通过查询来推导。这让数据模型更简洁,更新操作更安全,也更容易理解。
当然,在极端的读密集型场景下,比如一个拥有数亿用户的社交平台,每秒有数万次好友列表查询,那么可能会考虑引入缓存层(如Redis)来存储预计算的好友关系,或者使用专门的图数据库。但在关系型数据库层面,保持非冗余是最好的起点。
处理“取消关注”或“拉黑”等操作,数据层面如何优雅实现?处理这些用户关系变更操作,我们同样可以在
is_mutual表的基础上进行,并可能引入一个辅助表来处理“拉黑”这种更复杂的场景。
取消关注 (Unfollow): 这是最直接的操作。当用户A取消关注用户B时,我们只需要删除
SELECT u.id, u.username -- 假设有一个users表 FROM users u JOIN user_relationships ur1 ON u.id = ur1.following_id JOIN user_relationships ur2 ON u.id = ur2.follower_id WHERE ur1.follower_id = [用户X的ID] AND ur2.following_id = [用户X的ID] AND ur1.follower_id = ur2.following_id; -- 确保是互相关注的同一个对象表中对应的记录即可。
SELECT T1.following_id AS friend_id FROM user_relationships T1 JOIN user_relationships T2 ON T1.follower_id = T2.following_id AND T1.following_id = T2.follower_id WHERE T1.follower_id = [用户X的ID];
这个操作非常简单,也符合我们之前强调的原子性原则。如果A和B之前是互相关注的,那么删除这条记录后,他们就不再是互相关注了(因为
SELECT COUNT(*) AS fan_count FROM user_relationships WHERE following_id = [用户X的ID] AND status = 'active';记录不存在了),而
SELECT COUNT(*) AS following_count FROM user_relationships WHERE follower_id = [用户X的ID] AND status = 'active';记录依然存在,表示B仍然关注A。
拉黑 (Block): “拉黑”通常比“取消关注”要复杂一些,它通常意味着:
- 被拉黑者不能关注拉黑者。
- 如果被拉黑者已经关注了拉黑者,这个关注关系应该被解除。
- 拉黑者通常也不会再关注被拉黑者(如果之前有的话),并且不能再次关注。
为了处理这种多层面的逻辑,我倾向于引入一个单独的
IN表,而不是仅仅依赖
SELECT ur1.following_id AS common_following_id FROM user_relationships ur1 JOIN user_relationships ur2 ON ur1.following_id = ur2.following_id WHERE ur1.follower_id = [用户A的ID] AND ur2.follower_id = [用户B的ID] AND ur1.status = 'active' AND ur2.status = 'active';表的
follower_id字段。为什么?因为“拉黑”是一种比“关注”更强烈的、具有单向阻断性质的行为,它可能涉及更多的业务逻辑和权限控制。
following_id表结构:
索引和唯一约束:
-
唯一约束:
is_mutual
- 确保一个用户不能重复拉黑另一个用户。
-
索引:
is_mutual
,(A, B)
拉黑操作的实现逻辑:
当用户A拉黑用户B时,通常会执行以下步骤:
-
在
(B, A)
表中插入记录:(A, B)
-
解除A对B的关注(如果存在):
is_mutual
-
解除B对A的关注(如果存在):
true
通过这种方式,我们清晰地分离了“关注”和“拉黑”这两种行为,使得业务逻辑更加清晰。
(B, A)表的存在,也方便我们快速查询某个用户拉黑了谁,或者某个用户被谁拉黑了。在查询用户可见内容时,需要额外增加逻辑来排除被拉黑用户或拉黑了当前用户的用户。这种设计虽然增加了表的数量,但却避免了在
is_mutual表中过度承载复杂状态,保持了核心表的简洁性。
true
(B, A)
(A, B)
is_mutual
false
is_mutual
follower_id
following_id
user_relationships
user_relationships
DELETE FROM user_relationships WHERE follower_id = [用户A的ID] AND following_id = [用户B的ID];
(A, B)
(B, A)
user_blocks
user_relationships
status
user_blocks
id
BIGINT
PRIMARY KEY
AUTO_INCREMENT
blocker_id
BIGINT
NOT NULL
FOREIGN KEY
users.id
blocked_id
BIGINT
NOT NULL
FOREIGN KEY
users.id
created_at
TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP
UNIQUE KEY (blocker_id, blocked_id)
INDEX (blocker_id, blocked_id)
INDEX (blocked_id)
user_blocks
INSERT INTO user_blocks (blocker_id, blocked_id) VALUES ([用户A的ID], [用户B的ID]);
DELETE FROM user_relationships WHERE follower_id = [用户A的ID] AND following_id = [用户B的ID];
DELETE FROM user_relationships WHERE follower_id = [用户B的ID] AND following_id = [用户A的ID];
user_blocks
user_relationships
以上就是如何设计一个好友关系链数据库表结构?(双向关注、粉丝模型)的详细内容,更多请关注知识资源分享宝库其它相关文章!
相关标签: mysql redis win 为什么 red sql 数据类型 NULL default redis 数据库 大家都在看: MySQL内存使用过高(OOM)的诊断与优化配置 MySQL与NoSQL的融合:探索MySQL Document Store的应用 如何通过canal等工具实现MySQL到其他数据源的实时同步? 使用Debezium进行MySQL变更数据捕获(CDC)实战 如何设计和优化MySQL中的大表分页查询方案
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。