简化Django Query Annotation 减少统计新Forum消息的SQL查询次数
统计新Forum消息的低查询实现方案
你现在遇到的是典型的N+1查询问题:1次查询拉取Info列表,遍历每个Info时各产生1次InfoViewed查询和1次Forum查询,100条Info就会产生200+次查询,完全可以通过Django ORM的聚合、跨表查询功能优化到仅1次SQL查询。
方案1:直接统计总新消息数(仅1次查询)
直接从Forum表发起查询,通过跨表关联筛选符合条件的记录,代码如下:
from django.db.models import F, Q # 你只需要修改x为实际的用户ID,self.user为当前访问的用户对象 new_forum_total = Forum.objects.filter( # 筛选Forum关联的Info属于目标用户 info__user_id=x, # 筛选消息时间晚于用户上次浏览时间 datum__gt=F("info__infoviewed__last_seen"), # 确保浏览记录是对应当前访问用户的 info__infoviewed__user=self.user ).count() # 如果需要兼容没有浏览记录的情况(用户第一次访问,所有消息都算新),改为下面的写法: new_forum_total = Forum.objects.filter( info__user_id=x, Q( info__infoviewed__user=self.user, datum__gt=F("info__infoviewed__last_seen") ) | Q( info__infoviewed__isnull=True ) ).count()
方案2:同时获取每个Info的新消息数(仅1次查询)
如果你除了总数之外,还需要用到每个Info对应的新消息数量,可以用annotate给每个Info附加新消息数字段:
from django.db.models import Count, F, Q infos = Info.objects.filter(user_id=x).annotate( new_forum_count=Count( "forum", filter=Q( forum__datum__gt=F("infoviewed__last_seen"), infoviewed__user=self.user ) ) ) # 直接在内存中累加,不需要额外查询 total = sum(info.new_forum_count for info in infos)
注意事项
- 如果你给外键自定义了
related_name,需要把上面代码里的反向关联名(比如infoviewed、forum)替换为你自己定义的名称。 - Django 1.11原生支持Count的filter参数和F表达式跨表比较,上述代码完全兼容你的版本。
- 原代码中直接取
info_viewed[0],默认每个用户+每个Info仅对应一条浏览记录,建议给InfoViewed加联合唯一约束避免数据重复:
class InfoViewed(models.Model): user = models.ForeignKey(User) infokom = models.ForeignKey(Info) last_seen = models.DateTimeField() class Meta: unique_together = ("user", "infokom")
内容的提问来源于stack exchange,提问作者user2194805
相关产品推荐
相关产品推荐

