【问题标题】:List of parents objects and their children with fewer queries查询较少的父对象及其子对象列表
【发布时间】:2012-09-30 14:42:24
【问题描述】:

我有一个我正在尝试优化的 Django 视图。它在页面上显示父对象及其子对象的列表。子模型将外键返回给父模型,因此 select_related 似乎不适用。

class Parent(models.Model):
    name = models.CharField(max_length=31)

class Child(models.Model):
    name = models.CharField(max_length=31)
    parent = models.ForeignKey(Parent)

一个简单的实现使用 n+1 个查询,其中 n 是父对象的数量,即。一个查询获取父列表,然后一个查询获取每个父列表的子。

我编写了一个视图,它在两个查询中完成这项工作 - 一个用于获取父对象,另一个用于获取相关的子对象,然后是一些 Python(我不好意思在此发布)将其全部又复合了。

当我发现自己导入了标准库的collections 模块后,我意识到我可能做错了。可能有一个更简单的方法,但我缺乏 Django 经验来找到它。任何指针将不胜感激!

【问题讨论】:

    标签: python django query-optimization django-queryset


    【解决方案1】:

    在外键中添加related_name,然后使用Django 1.4中添加的prefetch_related方法:

    返回一个QuerySet,它将自动检索,在单个 每个指定查找的批处理相关对象。

    这与select_related 的目的相似,因为两者都是 旨在阻止由以下原因引起的大量数据库查询 访问相关对象,但策略大不相同:

    • select_related 通过创建 SQL 连接并包含字段来工作 SELECT 语句中的相关对象。为此原因, select_related 获取同一数据库查询中的相关对象。 但是,为了避免产生更大的结果集 加入“多”关系,select_related 仅限于 单值关系 - 外键和一对一。

    • prefetch_related,另一方面,对每个 关系,并在 Python 中“加入”。这允许它 预取多对多和多对一对象,这是做不到的 使用select_related,除了外键和一对一 select_related 支持的关系。它还支持 GenericRelationGenericForeignKey 的预取。

    class Parent(models.Model):
        name = models.CharField(max_length=31)
    
    class Child(models.Model):
        name = models.CharField(max_length=31)
        parent = models.ForeignKey(Parent, related_name='children') 
    
    
    >>> Parent.objects.all().prefetch_related('children')
    

    将在单个查询中获取所有相关子项,并使用 使查询集具有相关的预填充缓存 结果。然后在self.children.all() 中使用这些查询集 来电。

    注意 1,与 QuerySet 一样,任何暗示不同数据库查询的后续链接方法都将先前忽略 缓存结果,并使用新的数据库查询检索数据。

    注意 2 如果您使用 iterator() 运行查询,prefetch_related() 调用将被忽略,因为这两个 优化在一起没有意义。

    【讨论】:

    • 我已经阅读了您在此处引用的 Django 文档。虽然prefetch_related 似乎符合我的要求,但将其应用于我的查询并没有改变查询的数量 - 也许我做错了......你能提供一个示例调用吗?
    • 刚刚检查了我的项目类似案例 - 恰好执行了 2 个请求。最有可能的问题是,您如何从父母那里选择孩子。您不能使用filter 方法。这导致了一个新的请求。使用parent.children.all()
    • 另外,来自 doc:请注意,如果您使用 iterator() 运行查询,prefetch_related() 调用将被忽略,因为这两个优化一起没有意义。
    • 我在让您的示例正常工作时遇到了很多麻烦,但最终通过在外键中添加 related_name 来管理它。我将编辑您的答案以供将来参考。非常感谢您的帮助!
    【解决方案2】:

    其实,select_related 就是你要找的。 select_related 创建一个 JOIN,以便在一个语句中获取您需要的所有数据。 prefetch_related 一次运行所有查询然后缓存它们。

    这里的诀窍是只“加入”你绝对需要的东西,以减少加入的性能损失。“你绝对需要的东西”是一个很长的说法您应该只预先选择稍后将在视图或模板中阅读的字段。这里有很好的文档:https://docs.djangoproject.com/en/1.4/ref/models/querysets/#select-related

    这是我遇到类似问题的一个模型的 sn-p:

    return QuantitativeResult.objects.select_related(
                    'enrollment__subscription__configuration__analyte', 
                    'enrollment__subscription__unit', 
                    'enrollment__subscription__configuration__analyte__unit',
                    'enrollment__subscription__lab',
                    'enrollment__subscription__instrument_model'
                    'enrollment__subscription__instrument',
                    'enrollment__subscription__configuration__method',
                    'enrollment__subscription__configuration__reagent',
                    'enrollment__subscription__configuration__reagent__manufacturer',
                    'enrollment__subscription__instrument_model__instrument__manufacturer'
                    ).filter(<snip, snip - stuff edited out>)
    

    在这个病态的案例中,我从 700 多个查询减少到只有一个。 django 调试工具栏是您解决此类问题的好帮手。

    【讨论】:

    • 这是一个令人印象深刻的查询缩减!但是select_related 是否允许在“反向”方向上跟随外键? (我同意 django 调试工具栏是一个非常宝贵的工具...)
    • 我无法反向跟随 FK。在一个麻烦的情况下,我不得不退回到原始 SQL(随之而来的可移植性损失)
    【解决方案3】:

    如果您需要一次处理超过 2 个级别,您可以考虑使用 MPTT 将树存储在 db 中的不同方法

    简而言之,它将数据添加到您的模型中,这些数据在更新期间会更新,并允许更有效的检索。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-12-04
      • 1970-01-01
      • 2019-07-19
      • 2023-03-17
      • 2011-09-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多