基于Django:使用PostgreSQL ArrayField存储点赞的方案探讨
背景与需求
我有两个模型:Post和Comment,两者均可被User点赞。核心需求如下:
- 需在每个
Post或Comment附近显示总点赞数; - 每个
User需拥有展示其所有点赞内容的页面。
最直观的方案是使用多对多(m2m)字段,但担心未来会引发诸多问题。因此提出以下替代方案:
- 为
Post和Comment模型添加字段:users_liked_ids = ArrayField(models.IntegerField()); - 为
User模型添加字段:posts_liked_ids = ArrayField(models.IntegerField())、comments_liked_ids = ArrayField(models.IntegerField())。
用户点赞时执行两个操作:
- 将用户ID添加至对应
Post/Comment的users_liked_ids字段; - 将
Post/Comment的ID添加至对应用户的posts_liked_ids/comments_liked_ids字段。
现提出以下问题:
- 该方案是否可行?
- 此方案下查询“当前用户是否已点赞某篇Post/Comment”的效率如何?
- 将点赞数据存储在单独表的ArrayField中是否比存在被点赞模型中更好?
- 是否应该继续使用直观的多对多字段方案?
1. 该方案是否可行?
短期来看确实能跑通,满足你当前的两个核心需求:统计点赞数直接取数组长度,展示用户点赞内容可以从用户的数组字段里取ID查询。但这个方案藏着不少隐患:最突出的是数据一致性问题——点赞时必须同时更新两个地方的数组,一旦中间出现异常(比如事务失败、代码逻辑遗漏),就会出现“用户以为点赞成功但内容的点赞列表没记录”或者反过来的情况,后期排查修复会非常头疼。另外,当数组长度变大(比如热门帖子有上万点赞),存储和查询的开销会直线上升。
2. 查询当前用户是否已点赞的效率如何?
效率会随着数据量增长急剧下滑。因为ArrayField的包含查询(比如检查用户ID是否在users_liked_ids里)本质是全数组扫描,数据库无法给数组内的单个元素建索引。点赞数少的时候可能没感觉,但如果是有几千上万个点赞的热门内容,每次查询都要遍历整个数组,延迟会变得很高。反过来,查用户是否点赞了某篇内容,从用户的posts_liked_ids里检索也是同样的问题——用户点赞的内容越多,扫描的数组越长,速度越慢。
3. 将点赞数据存储在单独表的ArrayField中是否比存在被点赞模型中更好?
并没有本质改善。不管是单独建表存user_id+target_ids数组,还是target_id+user_ids数组,依然逃不开数组扫描的性能问题,还多了一层表关联的开销。而且数据一致性的问题依然存在——你还是要维护用户侧和内容侧的关联,只是换了个存储位置而已。
4. 是否应该继续使用直观的多对多字段方案?
非常建议回到多对多方案(或专门的中间表方案),Django的m2m字段本质就是自动生成中间表,这才是更合理的选择,原因如下:
- 数据一致性:通过数据库事务和外键约束,能保证点赞关系的完整性,不会出现两边数据不一致的情况;
- 查询效率:可以给中间表的
user_id+target_id(加上内容类型区分)建联合索引,不管是查用户是否点赞某内容、统计内容点赞数,还是查询用户所有点赞内容,都能通过索引快速定位,数据量再大也能保持高效; - 扩展性:未来如果要给点赞加额外属性(比如点赞时间、是否取消点赞),中间表可以直接加字段,而ArrayField方案几乎没法扩展;
- 维护成本:Django的ORM对多对多关系有完善封装,查询、更新操作都很简洁,后期维护比手动维护两个数组轻松太多。
你之前担心的“未来问题”,大多可以通过合理设计中间表和索引避免。比如担心性能就建联合索引;要区分Post和Comment,可以用Django的ContentType框架在中间表里加content_type字段,实现通用的点赞关系,不用给两个模型分别建m2m字段。
内容的提问来源于stack exchange,提问作者MaxCore

