Django高效关联查询过滤特定行:获取作者最新书籍信息
问题描述
假设有以下Django模型:
from django.db import models class Author(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) class Book(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=128) date = models.DateTimeField() author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
需要实现一个高效的查询集,获取所有作者,并为每个作者标注其最新书籍的名称和日期。要求尽可能避免为每个所需字段创建单独的子查询(实际场景中需要获取更多书籍字段),优先通用过滤方案,针对最新日期的特定方案也可接受。
已尝试的方案
Subquery方案
可以用子查询实现,但子查询只能返回单列,因此每个书籍字段都需要单独创建子查询,效率极低:
books = Book.objects.filter(author=OuterRef('pk')).order_by('-date') qs = Author.objects.all().annotate( latest_book_name=Subquery(books.values('name')[:1]), latest_book_date=Subquery(books.values('date')[:1]), )
Prefetch方案
通过prefetch_related获取作者关联的所有书籍,并用Max标注最新书籍日期,但无法同时获取对应书籍的名称:
qs = Author.objects.all().prefetch_related(Prefetch("books")) qs = qs.annotate(latest_book_date=Max("books__date"))
Django中没有直接的Argmax运算符,无法通过此方法关联到对应书籍的其他字段。
first/latest方法
first()或latest()返回单个对象,无法作为标注值添加到查询集中,不适用。
匹配最大ID方案
部分场景通过标注Max("books__id")并匹配书籍ID来筛选最新书籍,但本场景中书籍并非按时间顺序添加(实际甚至使用UUID作为主键),此方案不适用。
高效解决方案
方案1:使用Window函数(推荐,Django 2.0+)
利用Window和Rank函数为每个作者的书籍按日期倒序排名,筛选出排名为1的最新书籍,再与作者表关联,一次查询即可获取所有所需字段:
from django.db.models import Window, F from django.db.models.functions import Rank # 为书籍按作者分组,按date倒序排名 ranked_books = Book.objects.annotate( rank=Window( expression=Rank(), partition_by=F('author_id'), order_by=F('date').desc() ) ).filter(rank=1) # 关联作者和最新书籍,获取所需字段 qs = Author.objects.annotate( latest_book_name=F('books__name'), latest_book_date=F('books__date') ).filter(books__in=ranked_books).distinct()
此方案仅需少量数据库查询,且无需为每个字段单独创建子查询,适合需要获取多个书籍字段的场景。
方案2:子查询关联最新日期的书籍
先通过子查询获取每个作者的最新书籍日期,再关联书籍表获取对应字段,避免多字段子查询:
from django.db.models import Subquery, OuterRef, Max # 获取每个作者的最新书籍日期 latest_dates = Author.objects.annotate( latest_date=Max('books__date') ).values('pk', 'latest_date') # 筛选出每个作者最新日期对应的书籍 latest_books = Book.objects.filter( author_id=OuterRef('pk'), date=OuterRef('latest_date') ) # 标注最新书籍的字段 qs = Author.objects.annotate( latest_book_name=Subquery(latest_books.values('name')[:1]), latest_book_date=Subquery(latest_books.values('date')[:1]) )
此方案仅需两次子查询(而非每个字段一次),效率优于原始的多字段子查询方案,适合无法使用Window函数的低版本Django。
内容的提问来源于stack exchange,提问作者Jean Bouvattier
相关产品推荐
相关产品推荐

