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】:

我们可以过滤Invoices,当我们执行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 查询集优化 - 防止选择带注释的字段的主要内容,如果未能解决你的问题,请参考以下文章

Django 在不知道文件名的情况下显示带注释的查询集的值

在 Django 中使用带注释的查询集运行总计

Django - 查询:使用相关模型字段注释查询集

带有字段比较结果的django注释查询集

Django:通过注释字段的总和订购查询集?

带有注释的Django查询集,为啥将GROUP BY应用于所有字段?