【问题标题】:Django querysets optimization - preventing selection of annotated fieldsDjango 查询集优化 - 防止选择带注释的字段
【发布时间】: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。 有没有更好的方法来做到这一点?

【问题讨论】:

    标签: python django django-models django-queryset django-annotate


    【解决方案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 的另一个技巧非常酷,而且效果很好!很好的发现!
        猜你喜欢
        • 2013-08-28
        • 2018-06-02
        • 2020-05-14
        • 1970-01-01
        • 2021-10-17
        • 2015-09-19
        • 2017-08-29
        • 2020-06-13
        • 1970-01-01
        相关资源
        最近更新 更多