Django优化:如何减少数据库查询次数?附相关代码示例
如何减少Django项目中因统计浏览量产生的数据库查询次数?
你遇到的是典型的N+1查询问题:遍历10篇文章时,每篇文章调用views_count属性都会触发一次独立的COUNT查询,额外产生10次SQL请求。下面是几种高效的解决方案:
方案1:使用annotate预聚合统计(推荐)
直接在查询Post时,用Django的聚合函数Count一次性计算出每篇文章的浏览量,把统计结果整合到初始查询中,彻底避免N+1问题。
修改views.py中的查询代码:
from django.db.models import Count posts = Post.objects.filter(is_toplevel=True, status=Post.ACTIVE) \ .select_related('author', 'category') # 合并多个外键的select_related .prefetch_related('tags') \ .annotate(views_count=Count('post_views_count')) # 关联PostViews的related_name做统计
之后模板里可以直接用{{ post.views_count }},如果要保留模型中的@property,建议修改它优先使用annotate生成的字段:
@property def views_count(self): # 优先用annotate的统计结果,没有的话再执行查询 return getattr(self, 'views_count', PostViews.objects.filter(post=self).count())
方案2:给Post模型添加缓存字段
如果浏览量不需要实时精确统计(比如允许延迟更新),可以在Post模型中新增一个存储浏览量的字段,每次新增浏览记录时更新这个字段,访问时直接读取字段值,完全不需要查询关联表。
修改模型:
class Post(models.Model): # 原有字段... views_count = models.IntegerField(default=0) # 新增缓存字段 class PostViews(models.Model): # 原有字段... def save(self, *args, **kwargs): is_new = self.pk is None super().save(*args, **kwargs) if is_new: # 仅新增浏览记录时更新对应Post的计数 self.post.views_count = PostViews.objects.filter(post=self.post).count() self.post.save(update_fields=['views_count'])
或者用Django信号解耦逻辑(更优雅):
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=PostViews) def update_post_views_cache(sender, instance, created, **kwargs): if created: # 新增记录时更新缓存字段 instance.post.views_count = instance.post.post_views_count.count() instance.post.save(update_fields=['views_count'])
模板中直接使用{{ post.views_count }}即可,性能最优。
方案3:预取关联数据后计算长度
通过Prefetch对象预取所有关联的PostViews记录到内存列表,之后通过列表长度获取计数,避免重复执行COUNT查询:
修改views.py:
from django.db.models import Prefetch posts = Post.objects.filter(is_toplevel=True, status=Post.ACTIVE) \ .select_related('author', 'category') \ .prefetch_related( 'tags', Prefetch( 'post_views_count', queryset=PostViews.objects.all(), to_attr='_cached_post_views' ) )
修改模型中的views_count属性:
@property def views_count(self): return len(getattr(self, '_cached_post_views', []))
这种方式适合需要同时获取浏览记录详情的场景,单纯统计计数的话不如方案1高效。
内容的提问来源于stack exchange,提问作者qwerty
相关产品推荐
相关产品推荐

