收藏功能必须拆成独立表,不能加到用户或内容表里
直接在
users表加个
favorite_ids字段(比如存 CSV)或者在
articles表加
favorited_by,短期看着省事,但很快会卡死:查某用户所有收藏、查某内容被多少人收藏、取消收藏时更新字符串都难做且慢。关系型数据库的强项是关联查询,不是字符串解析。
正确做法是建一张中间表:
user_favorites,至少包含三个字段:
id(主键,非必需但建议)、
user_id(外键指向
users.id)、
item_id(指向被收藏的内容,比如
articles.id或
products.id),再加一个
item_type字段支持多类型(可选,见下一点)。 如果只收藏一种类型(比如全是文章),
item_id直接关联
articles.id,加联合唯一索引
(user_id, item_id)防重复收藏 如果要同时收藏文章、视频、商品,就用「泛型外键」:加
item_type(如
'article'、
'video'),并确保应用层校验
item_id确实存在对应记录——MySQL 本身无法对动态表名建外键 别忘了给
user_id单独建索引,否则按用户查收藏列表会全表扫描
查用户收藏列表时,JOIN 比子查询更稳
想查用户 ID=123 的所有收藏文章及标题,常见写法是:
SELECT * FROM articles WHERE id IN (SELECT item_id FROM user_favorites WHERE user_id = 123)。这在数据量小的时候没问题,但一旦
user_favorites有几十万行,MySQL 可能不走
user_id索引,或生成临时表拖慢查询。
更可靠的是显式
JOIN:
SELECT a.* FROM user_favorites uf JOIN articles a ON uf.item_id = a.id WHERE uf.user_id = 123 ORDER BY uf.id DESC LIMIT 20;
uf.id DESC是按收藏时间倒序(假设用自增主键记录顺序),比用
created_at更轻量;如果需要精确时间,就加
created_at字段并索引它 务必在
user_favorites(user_id, id)上建联合索引,让排序和 LIMIT 能用上索引 如果收藏量极大(比如用户收藏了 10 万条),考虑分页改用游标(cursor-based pagination),避免
LIMIT 100000, 20
取消收藏必须用 DELETE,不是 UPDATE
看到有人把
user_favorites表设计成带
is_deleted字段的软删除,这是错的。收藏本质是「存在性关系」,不是「状态记录」。软删除会让表越积越大,唯一约束失效(同一用户可重复插入已“删”的记录),查询逻辑变复杂。 取消收藏就一条语句:
DELETE FROM user_favorites WHERE user_id = 123 AND item_id = 456执行前不用先
SELECT判断是否存在——直接
DELETE返回影响行数,0 表示本来就没收藏,应用层据此返回提示即可 确保该语句命中
(user_id, item_id)联合索引,否则可能锁整张表(尤其在高并发取消场景)
注意事务边界和并发冲突
用户点「收藏」按钮时,典型流程是:先查是否已收藏 → 没有则插入。这个两步操作在并发下会出问题:两个请求几乎同时查,都得到「未收藏」,然后都插入,违反唯一约束。
最简单解法:忽略唯一键冲突。用INSERT IGNORE INTO user_favorites (user_id, item_id) VALUES (123, 456),冲突时静默失败,应用层按影响行数判断是否成功 更严谨的做法是用
INSERT ... ON DUPLICATE KEY UPDATE,但这里不需要更新任何字段,所以
IGNORE更轻量 如果业务要求严格返回「本次是否新增」,那就得用事务 +
SELECT ... FOR UPDATE锁住用户行,但会降低并发能力,一般没必要
泛型外键(
item_type)带来的校验责任完全落在应用层,数据库不兜底;而唯一索引和外键缺失的地方,就是线上出错的第一现场。
