使用 annotate Exists 时提高 Django 查询集性能
Posted
技术标签:
【中文标题】使用 annotate Exists 时提高 Django 查询集性能【英文标题】:Improve Django queryset performance when using annotate Exists 【发布时间】:2019-11-27 22:14:58 【问题描述】:我有一个返回大量数据的查询集,它可以按年份过滤,返回大约 100k 行,或者显示所有将带来大约 100 万行。
此注释的目的是生成一个 xlsx 电子表格。
模型表示,RelatedModel
是 Model
和 AnotherModel
之间的多对数
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 查询集性能的主要内容,如果未能解决你的问题,请参考以下文章
Django:替代使用 annotate(Count()) 来提高速度
Oracle,用left join 替代 exists ,not exists,in , not in,提高效率
关于sql中的exists,写上和不写是否没啥区别,是否只是提高了执行效率