
用单条记录表示双向关系,别存两条
MySQL 里好友关系本质是无向图边,user_a_id 和 user_b_id 之间没有主从之分。如果对每对好友存两条记录(A→B 和 B→A),后续查“所有好友”要 UNION 或用 OR,索引也难优化,还容易因逻辑不一致导致数据错乱。
正确做法是强制规定:每条记录中 user_a_id user_b_id(数值比较)。插入前先排序两个 ID,再写入。这样每对关系只有一条物理记录,查询、去重、索引都干净。
- 插入时用
LEAST(user_id1, user_id2)和GREATEST(user_id1, user_id2)自动归一化 - 联合唯一索引必须建在
(user_a_id, user_b_id)上,且顺序固定,否则无法拦截重复插入 - 避免用字符串拼接(如
CONCAT(LEAST(...), '-', GREATEST(...)))做唯一键——没法走索引,也增加存储和比对开销
查“我的所有好友”必须用 OR + 覆盖索引
虽然只存一条记录,但用户 A 的好友可能出现在 user_a_id = A 或 user_b_id = A 任一字段里。直接 WHERE user_a_id = ? OR user_b_id = ? 在旧版本 MySQL(
更可靠的方式是建覆盖索引,并用 UNION ALL 拆成两个走索引的查询:
SELECT user_b_id AS friend_id FROM friends WHERE user_a_id = 123 UNION ALL SELECT user_a_id AS friend_id FROM friends WHERE user_b_id = 123;
- 索引要建两份:
(user_a_id, user_b_id)和(user_b_id, user_a_id),缺一不可 -
UNION ALL比UNION快,因为不校验去重——我们已确保不会重复(单边查,且原始表无冗余) - 别依赖
INDEX_MERGE提示,它在某些执行计划下反而变慢,尤其数据量大时
加好友请求状态不能塞进同一张表
好友关系表一旦设计为“已确立关系”,就该保持原子性和稳定性。status 字段如果同时承载 pending / accepted / rejected,会导致主键约束失效(比如两个 pending 请求会因归一化被当成同一条而冲突),也会让“查待处理请求”这类查询变得绕弯。
真正可扩展的做法是拆表:
- 主表
friends只存accepted状态的双向关系,无 status 字段,主键即(user_a_id, user_b_id) - 单独建
friend_requests表,含from_user_id、to_user_id、status、created_at,主键为(from_user_id, to_user_id) - 双方确认后,从
friend_requests删除记录,并向friends插入归一化后的单条记录
外键和级联删除非常危险
别给 friends.user_a_id 或 friends.user_b_id 加外键并设 ON DELETE CASCADE。用户注销时若触发级联,会连带删掉所有涉及他的好友关系——哪怕对方根本没注销,这违反社交关系的语义。
正确做法是业务层控制:
- 用户注销仅软标记(
is_deleted = 1),不删行;好友关系表查询时自动过滤掉已注销用户 - 真要物理删除用户,必须先手动清理其在
friends和friend_requests中的所有引用,再删用户主表 - 如果硬要用外键,至少选
ON DELETE RESTRICT,让 DB 主动报错,倒逼业务代码显式处理
归一化逻辑、双索引、状态拆分、软删策略——这些不是“最佳实践”名词,而是每次写错就会立刻翻车的具体动作点。漏掉任意一个,后面加字段、改查询、扛流量时都会回来找你。










