mysql如何实现用户收藏列表_mysql关系设计实战

来源:这里教程网 时间:2026-02-28 20:47:18 作者:

收藏功能必须拆成独立表,不能加到用户或内容表里

直接在

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
)带来的校验责任完全落在应用层,数据库不兜底;而唯一索引和外键缺失的地方,就是线上出错的第一现场。

相关推荐