如何减少Django查询子模型属性时的数据库调用次数?
你遇到的是典型的N+1查询陷阱,尤其是offer_set.latest()每次调用都会触发单独的数据库查询,这才导致了400次额外查询。之前用prefetch_related没解决问题,是因为latest()本质上是执行ORDER BY created DESC LIMIT 1的数据库查询,并不会复用预取到内存的Offer数据。下面给你两个高效的解决方案:
方案一:用Subquery直接在Venue查询中注解最新Offer字段
这个方案只需要1次数据库查询,直接在查询Venue的时候就把每个Venue对应的最新Offer的created和content字段取出来,完全避免额外查询。
修改你的视图代码:
from django.db.models import Subquery, OuterRef class MapView(ListView): fields = ["name"] model = Venue template_name = "venues/venue_map.html" def get_queryset(self): # 定义子查询:获取当前Venue的最新Offer(按created倒序取第一条) latest_offer_subquery = Offer.objects.filter( venue_associated=OuterRef('pk') ).order_by('-created')[:1] # 给Venue查询集注解两个字段:最新Offer的创建时间和内容 return Venue.objects.annotate( latest_offer_created=Subquery(latest_offer_subquery.values('created')), latest_offer_content=Subquery(latest_offer_subquery.values('content')) )
修改模板,直接使用注解后的字段,不用再调用offer_set.latest():
{% for venue in venue_list %} {{ venue.name }} {{ venue.latest_offer_created }} {{ venue.latest_offer_content }} {% endfor %}
方案二:用Prefetch预取每个Venue的最新Offer
这个方案会产生2次数据库查询(一次查所有Venue,一次批量查所有Venue的最新Offer),但如果之后需要用到Offer的其他字段,会更灵活。
修改视图代码:
from django.db.models import Prefetch class MapView(ListView): fields = ["name"] model = Venue template_name = "venues/venue_map.html" def get_queryset(self): # 定义预取的查询集:按created倒序,这样每个Venue的Offer列表里第一个就是最新的 latest_offers_queryset = Offer.objects.order_by('-created') # 使用Prefetch指定预取的关联关系,把结果存到`latest_offer`属性里 return Venue.objects.prefetch_related( Prefetch( 'offer_set', queryset=latest_offers_queryset, to_attr='latest_offer' ) )
模板里访问预取到的最新Offer(注意latest_offer是列表,取第一个元素):
{% for venue in venue_list %} {{ venue.name }} {% if venue.latest_offer %} {{ venue.latest_offer.0.created }} {{ venue.latest_offer.0.content }} {% endif %} {% endfor %}
为什么之前的prefetch_related没用?
默认的prefetch_related('offer_set')会把每个Venue的所有Offer都预取到内存,但venue.offer_set.latest()并不是从内存的列表里取最新的,而是会重新向数据库发送ORDER BY created DESC LIMIT 1的查询,这就是为什么你还是会有400次额外查询。而上面的两种方案,要么直接在数据库层面聚合数据,要么预取时就只获取最新的一条,彻底避免了重复查询。
另外,你的模型里已经给Offer.created加了db_index=True,这很好,能保证子查询和排序的效率,不用额外加索引了。
内容的提问来源于stack exchange,提问作者Erlend

