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

Posted

技术标签:

【中文标题】使用 annotate Exists 时提高 Django 查询集性能【英文标题】:Improve Django queryset performance when using annotate Exists 【发布时间】:2019-11-27 22:14:58 【问题描述】:

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

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

模型表示,RelatedModelModelAnotherModel 之间的多对数

Model:
    id
    field1
    field2
    field3

RelatedModel:
    foreign_key_model (Model)
    foreign_key_another (AnotherModel)

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'
)

【问题讨论】:

使用分页? 目标是生成一个电子表格(xlsx文件) 可以,可以分批写。 【参考方案1】:

如果只需要更改 True / False 在 xlsx 中的显示方式 - 一种选择是只使用一个 related_exists BooleanField 注释,然后在创建 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;

我们可以立即看到 Exists CLIENT_SELECT 的嵌套查询存在 3 次。即使完全一样,也可以执行最少 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!')

为什么related_column 再次执行相同的选择而不采用related_exists 的值?

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

等等,那么我们实际上不需要related_exists 列,让related_column 留下CASE 语句并且1 存在子查询。

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

所以,我们的例子是这样的:为了在When中使用Exist,我们首先需要将它添加为注解,但它不会用作参考,而是表达式的完整副本.


好消息!

由于Django 3.0,我们可以在查询集过滤器中直接使用输出 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(),
    )
)

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


Django 2.1、2.2

这是commit,它最终确定了允许布尔表达式,尽管之前添加了许多前提条件。其中之一是表达式对象上存在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(),
    )
)


相关检查

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

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

【讨论】:

setattr(related_model_exists, 'conditional', True) 不适用于 dj 2 =(【参考方案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()
    )
)

其中relatedmodel_set 是您外键上的related_name

【讨论】:

以上是关于使用 annotate Exists 时提高 Django 查询集性能的主要内容,如果未能解决你的问题,请参考以下文章

Java五种基本的Annotation,提高程序的可读性

Django:替代使用 annotate(Count()) 来提高速度

Oracle,用left join 替代 exists ,not exists,in , not in,提高效率

关于sql中的exists,写上和不写是否没啥区别,是否只是提高了执行效率

SQL优化--使用 EXISTS 代替 IN 和 inner join来选择正确的执行计划

JAVA提高五:注解Annotation