使用 annotate Exists 时提高 Django 查询集的性能

Hus*_*ell 6 django django-queryset query-performance

我有一个返回大量数据的查询集,它可以按年份过滤,将返回大约 10 万行,或显示所有将带来大约 100 万行。

此注释的目的是生成一个 xlsx 电子表格。

模型表示,RelatedModelModel和之间是多对多的AnotherModel

Model:
    id
    field1
    field2
    field3

RelatedModel:
    foreign_key_model (Model)
    foreign_key_another (AnotherModel)
Run Code Online (Sandbox Code Playgroud)

Queryset,如果关系存在,它会注释,这个注释很慢,可能需要几分钟。

Model.objects.all().annotate(
    related_exists=Exists(RelatedModel.objects.filter(foreign_key_model=OuterRef('id'))),
    related_column=Case(
        When(related_exists=True, then=Value('The relation exists!')),
        When(related_exists=False, then=Value('The relation doesn't exist!')),
        default=Value('This is the default value!'),
        output_field=CharField(),
    )
).values_list(
    'related_column',
    'field1',
    'field2',
    'field3'
)
Run Code Online (Sandbox Code Playgroud)

Ole*_*kin 6

如果只需要更改 xlsx 中 True / False 的显示方式 - 一种选择是只有一个related_existsBooleanField 注释,然后在创建 xlsx 文档时自定义它的转换方式 - 即在序列化程序中。数据库应存储原始/未格式化的值,应用程序准备将它们显示给用户。

其他需要考虑的事项:

  • 加速过滤的索引。
  • 如果过滤后有数百万条记录,在一张表中 - 也许可以考虑表分区。

但是让我们看看原始查询的原始 sql。它会是这样的:

SELECT [model_fields],
       EXISTS([CLIENT_SELECT]) AS related_exists,
       CASE
       WHEN EXISTS([CLIENT_SELECT]) = true THEN 'The relation exists!'
       WHEN EXISTS([CLIENT_SELECT]) = true THEN 'The relation does not exist!'
       ELSE 'The relation exists!'
       END AS related_column
FROM model;
Run Code Online (Sandbox Code Playgroud)

而且马上就可以看到嵌套查询已存在CLIENT_SELECT3次。即使完全相同,也可能最少执行 2 次,最多执行 3 次。数据库可能优化到比 3 倍快,但仍然没有优化到 1 倍。

首先,EXISTS返回 True 或 False,我们可以只留下一个检查它是否为 True,'The relation does not exist!'作为默认值。

    related_column=Case(
        When(related_exists=True, then=Value('The relation exists!')),
        default=Value('The relation does not exist!')
Run Code Online (Sandbox Code Playgroud)

为什么related_column再次执行相同的选择而不是取值related_exists

因为我们无法在计算另一列时引用计算列- 这是 django 知道并复制表达式的数据库级约束。

等等,那么我们实际上不需要related_exists列,让我们留下related_columnCASE 语句和 1 个存在子查询。

Django 来了——我们不能(直到 3.0)在过滤器中使用表达式而不先注释它们。

所以,在我们的情况下是这样的:为了使用Existin When,我们首先需要将它添加为注释,但它不会用作引用,而是表达式的完整副本。


好消息!

Django 3.0 开始,我们可以在 QuerySet 过滤器中直接使用输出 BooleanField 的表达式,而不必先注释. Exists是这样的 BooleaField 表达式之一。

Model.objects.all().annotate(
    related_column=Case(
        When(
            Exists(RelatedModel.objects.filter(foreign_key_model=OuterRef('id'))),
            then=Value('The relation exists!'),
        ),
        default=Value('The relation doesn't exist!'),
        output_field=CharField(),
    )
)

Run Code Online (Sandbox Code Playgroud)

并且只有一个嵌套选择和一个带注释的字段。


Django 2.1、2.2

这是最终确定允许布尔表达式的提交,尽管之前添加了许多先决条件。其中之一是conditional表达式对象上存在属性并检查此属性。

所以,虽然不建议使用,并没有测试它似乎很有点工作黑客Django的2.1,2.2(以前没有conditional检查,这将需要更深入的变化):

  • 创建Exists表达式实例
  • 猴子修补它 conditional = True
  • 将其用作When语句中的条件
related_model_exists = Exists(RelatedModel.objects.filter(foreign_key_model=OuterRef('id')))

setattr(related_model_exists, 'conditional', True)

Model.objects.all().annotate(
    related_column=Case(
        When(
            relate_model_exists,
            then=Value('The relation exists!'),
        ),
        default=Value('The relation doesn't exist!'),
        output_field=CharField(),
    )
)

Run Code Online (Sandbox Code Playgroud)

相关检查

relatedmodel_set__isnull=True检查适合有以下几个原因:

  • 它执行LEFT OUTER JOIN- 效率低于EXISTS
  • 它执行LEFT OUTER JOIN- 它连接表,这使它仅适用于 filter() 条件(不适用于注释 - When),并且仅适用于 OneToOne 或 OneToMany(一个在相关模型端)关系


sol*_*oke 2

您可以大大简化查询:

from django.db.models import Count
Model.objects.all().annotate(
    related_column=Case(
        When(relatedmodel_set__isnull=True, then=Value("The relation doesn't exist!")), 
        default=Value("The relation exists!"), 
        output_field=CharField()
    )
)
Run Code Online (Sandbox Code Playgroud)

relatedmodel_set你的外键在哪里related_name