有一个
Foo
模型有一个外键字段 bar
:
class Foo(models.Model):
...
bar = models.ManyToManyField('bar.Bar', related_name='some_bar')
...
此外
Foo
有 get_config()
方法,该方法返回其字段,包括 bar
,例如:
def get_config(self):
return {
...
'bar': map(lambda x: x.get_config(), self.bar.all())
...
现在数据库中有 10,000 行
Foo
。还有一些Bar
行。
尝试检索大约 10,000 个
Foo
的数据,包括嵌套的 Bar
数据:
query = Foo.objects.all().prefetch_related('bar')
return [obj.get_config() for obj in query]
查询执行大约 6 秒。如果没有
bar
字段 - 只有 400 毫秒。
预取似乎无法完全工作
bar.get_config()
似乎每个迭代步骤都会访问数据库。它应该简单地加载所有 Bar
对象一次,并从该栏查询获取配置以填充每个 foo
配置。
如果在
iterator()
循环中使用 for
,则调用几乎会停滞并花费数十秒:[obj.get_config() for obj in query.iterator()]
prefetch_related
正在工作。 Django 使用包含所有 Bar
中的 ids
的 WHERE 子句预取 Foo
。这个具有 10k id 的 SELECT 比 10k SELECTS 快得多,但比没有任何过滤器的单个 SELECT 慢得多。
虽然我还没有测试过它,但我怀疑您在查询中看到的大部分缓慢来自 python,而不是数据库查询。使用
prefetch_related(...)
时,会对所有 Foo 进行 1 次查询,对所有 Bar 进行 1 次查询。然后,Python 领域的 Django 将这些查询的结果拼接在一起。
它们的性能可能会根据您的设置细节(数据库引擎、索引、数据库主机规格、服务主机规格等)而有所不同。您需要进行一些分析才能深入了解您的瓶颈。
关于您在“想法”标题下提到的行为...Django 版本<4.1 在使用
prefetch_related
时忽略
.iterator()
。
请注意,如果您使用 iterator() 运行查询,则 prefetch_lated() 调用将被忽略,因为这两个优化一起没有意义。
在 Django 4.1
中添加了对混合
prefetch_related
和 iterator
的支持。如果您升级到 Django >=4.1,那么您可以再次尝试使用 .iterator()
。这可能不会提高您的总数据库时间,但可能会提高内存使用率,并避免服务主机瓶颈。