Django 查询集优化 - 防止选择带注释的字段
Posted
技术标签:
【中文标题】Django 查询集优化 - 防止选择带注释的字段【英文标题】:Django querysets optimization - preventing selection of annotated fields 【发布时间】:2020-05-09 13:29:43 【问题描述】:假设我有以下模型:
class Invoice(models.Model):
...
class Note(models.Model):
invoice = models.ForeignKey(Invoice, related_name='notes', on_delete=models.CASCADE)
text = models.TextField()
我想选择有一些注释的发票。我会像这样使用annotate
/Exists
编写它:
Invoice.objects.annotate(
has_notes=Exists(Note.objects.filter(invoice_id=OuterRef('pk')))
).filter(has_notes=True)
这很好用,只过滤带有注释的发票。但是,这种方法会导致字段出现在查询结果中,这我不需要并且意味着性能更差(SQL 必须执行子查询 2 次)。
我意识到我可以像这样使用extra(where=)
写这个:
Invoice.objects.extra(where=['EXISTS(SELECT 1 FROM note WHERE invoice_id=invoice.id)'])
这将产生理想的 SQL,但通常不鼓励使用 extra
/ 原始 SQL。
有没有更好的方法来做到这一点?
【问题讨论】:
【参考方案1】:好的,我刚刚在Django 3.0 docs 中注意到,他们已经更新了Exists
的工作方式,可以直接在filter
中使用:
Invoice.objects.filter(Exists(Note.objects.filter(invoice_id=OuterRef('pk'))))
这将确保子查询不会被添加到 SELECT 列中,这可能会带来更好的性能。
在 Django 3.0 中更改:
在以前的 Django 版本中,需要先注解,然后根据注解进行过滤。这导致带注释的值始终存在于查询结果中,并且通常导致查询需要更多时间来执行。
不过,如果有人知道 Django 1.11 的更好方法,我将不胜感激。我们真的需要升级:(
【讨论】:
【参考方案2】:我们可以过滤Invoice
s,当我们执行LEFT OUTER JOIN
时,没有NULL
作为Note
,并使查询不同(以避免返回相同的Invoice
两次)。
Invoice.objects.<b>filter(notes__isnull=False).distinct()</b>
【讨论】:
啊,对,我实际上在其他地方使用了这种模式,但我不能在我的实际情况中使用 - 我过度简化了示例,实际上我在存在子查询中使用select_for_update
和一些额外的过滤器.但是,由于我在最初的问题中没有提到这一点 - 我会接受这个作为答案。【参考方案3】:
如果您想从另一个表中获取主键引用存储在另一个表中的数据,这是最好的优化代码 Invoice.objects.filter(note__invoice_id=OuterRef('pk'),)
【讨论】:
【参考方案4】:您可以使用.values()
查询集方法从 SELECT 子句中删除注释。 .values()
的问题在于您必须枚举所有要保留的名称而不是要跳过的名称,并且 .values()
返回字典而不是模型实例。
Django 内部会跟踪已删除的注释
QuerySet.query.annotation_select_mask
。所以你可以用它告诉 Django,即使没有.values()
,也可以跳过哪些注释:
class YourQuerySet(QuerySet):
def mask_annotations(self, *names):
if self.query.annotation_select_mask is None:
self.query.set_annotation_mask(set(self.query.annotations.keys()) - set(names))
else:
self.query.set_annotation_mask(self.query.annotation_select_mask - set(names))
return self
然后你可以写:
invoices = (Invoice.objects
.annotate(has_notes=Exists(Note.objects.filter(invoice_id=OuterRef('pk'))))
.filter(has_notes=True)
.mask_annotations('has_notes')
)
从 SELECT 子句中跳过 has_notes
并仍然获得过滤的发票实例。生成的 SQL 查询将类似于:
SELECT invoice.id, invoice.foo FROM invoice
WHERE EXISTS(SELECT note.id, note.bar FROM notes WHERE note.invoice_id = invoice.id) = True
请注意,annotation_select_mask
是内部 Django API,可以在未来版本中更改而不会发出警告。
【讨论】:
我知道values
,但我需要查询集来返回模型实例。但是annotation_select_mask
的另一个技巧非常酷,而且效果很好!很好的发现!以上是关于Django 查询集优化 - 防止选择带注释的字段的主要内容,如果未能解决你的问题,请参考以下文章