Django 查询集优化 - 防止选择带注释的字段

Django querysets optimization - preventing selection of annotated fields

假设我有以下型号:

class Invoice(models.Model):
    ...

class Note(models.Model):
    invoice = models.ForeignKey(Invoice, related_name='notes', on_delete=models.CASCADE)
    text = models.TextField()

并且我想要select有一些注释的发票。我会像这样使用 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。 有更好的方法吗?

好的,我刚刚在 Django 3.0 docs 中注意到他们已经更新了 Exists 的工作方式并且可以直接在 filter 中使用:

Invoice.objects.filter(Exists(Note.objects.filter(invoice_id=OuterRef('pk'))))

这将确保子查询不会添加到 SELECT 列,这可能会带来更好的性能。

Changed in Django 3.0:

In previous versions of Django, it was necessary to first annotate and then filter against the annotation. This resulted in the annotated value always being present in the query result, and often resulted in a query that took more time to execute.

不过,如果有人知道 Django 1.11 的更好方法,我将不胜感激。我们真的需要升级 :(

我们可以过滤 Invoices,当我们执行 LEFT OUTER JOIN 时,没有 NULL 作为 Note,并使查询不同(以避免返回同样 Invoice 两次)。

Invoice.objects.<b>filter(notes__isnull=False).distinct()</b>

如果您想从另一个 table 获取数据,而主键引用存储在另一个 table 中,那么这是最佳优化代码 Invoice.objects.filter(note__invoice_id=OuterRef('pk'),)

您可以使用 .values() 查询集方法从 SELECT 子句中删除注释。 .values() 的问题是你必须枚举所有你想保留的名字而不是你想跳过的名字,.values() returns 字典而不是模型实例。

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 可以在未来版本中更改而不会发出警告。

我们应该可以使用以下方法清除注释字段。

Invoice.objects.annotate(
    has_notes=Exists(Note.objects.filter(invoice_id=OuterRef('pk')))
).filter(has_notes=True).query.annotations.clear()