【问题标题】:Django ORM Optimisation - annotating by count as well as the last entry for the annotate filterDjango ORM 优化 - 按计数以及注释过滤器的最后一个条目进行注释
【发布时间】:2020-05-13 15:51:43
【问题描述】:

我目前在 Post 模型上拥有以下属性:

@property
def compliments(self):
    compliments_by_kind = list(
        self.compliment_set.values(
            'kind'
        ).annotate(
            amount=Count('kind')
        ).values(
            'kind',
            'amount'
        )
    )

    for compliment_by_kind in compliments_by_kind:
        compliment_by_kind['last_giver'] = self.compliment_set.filter(
            kind=compliment_by_kind['kind']
        ).order_by(
            'created'
        ).last().giver.name

    return compliments_by_kind

这将返回以下列表数据结构:

[
  {
    'kind': 'unique', 
    'amount': 3, 
    'last_giver': 'Person 1'
  }, 
  {
    'kind': 'fresh', 
    'amount': 2, 
    'last_giver': 'Person 2'
  }, 
  {
    'kind': 'concept', 
    'amount': 3, 
    'last_giver': 'Person 3'
  }, 
  {
    'kind': 'lines', 
    'amount': 1, 
    'last_giver': 'Person 4'
  }
]

数据本身没有问题。在循环中执行查询的性能没有任何问题。

但是,循环方法 - 效率不高 - 对于每种类型(总共有 6 个,在一个之上还有 6 个进一步的查询来获得 Count 注释。所以,这确实妨碍了性能serialization舞台。

有没有人知道如何根据“种类”的“created”属性的排序来执行最新的Complimentgiver.name”的annotation......即最后一个人赞美“善良”独特等

这是Compliment 模型:

class Compliment(TimeStampedModel):
    giver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name="giver",
    )
    receiver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name="receiver",
    )
    post = models.ForeignKey('blog.post', on_delete=models.CASCADE)

    kind = models.CharField(choices=COMPLIMENTS_CHOICES, max_length=15)

更新:利用威廉的绝妙建议,我的解决方案是:

@property
def compliments(self):
    from django.contrib.auth import get_user_model
    from django.db.models import OuterRef, Subquery

    compliments_by_kind = list(
        self.compliment_set.values(
            'kind'
        ).annotate(
            amount=Count('kind'),
            first_name=Subquery(
                get_user_model().objects.filter(
                    giver__artwork_id=self.pk,
                    giver__kind=OuterRef('kind')
                ).values('first_name').order_by('-giver__created')[:1]
            ),
            last_name=Subquery(
                get_user_model().objects.filter(
                    giver__artwork_id=self.pk,
                    giver__kind=OuterRef('kind')
                ).values('last_name').order_by('-giver__created')[:1]
            ),
        )
    )

    return compliments_by_kind

【问题讨论】:

  • 您能否展示相关型号(Compliments,以及与.giver相关的型号?
  • @WillemVanOnsem 完全没有问题 - 已添加。 giver 与标准 Django User 模型相关。 kind 纯粹是一个 Char 字符串,假设有任意数量的 CHOICES

标签: python django django-orm


【解决方案1】:

您可以使用Subquery 在同一查询中获得last_giver(从而避免N+1 问题):

from django.contrib.auth import get_user_model
from django.db.models import OuterRef, Subquery

compliments_by_kind = list(
    self.compliment_set.values(
        'kind'
    ).annotate(
        amount=Count('kind')
        last_name=Subquery(
            get_user_model().objects.filter(
                giver__post_id=self.pk,
                giver__kind=OutRef('kind')
            ).values('name').order_by('giver__created')[:1]
        )
    )
)

话虽如此,正如 cmets 中所讨论的,related_name=… [Django-doc] 是“反向”关系的名称,因此是获得用户给予或接受的赞美的名称。因此,最好将它们重命名为:

class Compliment(TimeStampedModel):
    giver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='given_compliments',
    )
    receiver = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='received_compliments',
    )
    post = models.ForeignKey('blog.post', on_delete=models.CASCADE)
    kind = models.CharField(choices=COMPLIMENTS_CHOICES, max_length=15)

那么查询看起来像:

from django.contrib.auth import get_user_model
from django.db.models import OuterRef, Subquery

compliments_by_kind = list(
    self.compliment_set.values(
        'kind'
    ).annotate(
        amount=Count('kind')
        last_name=Subquery(
            get_user_model().objects.filter(
                given_compliments__post_id=self.pk,
                given_compliments__kind=OutRef('kind')
            ).values('name').order_by('giver__created')[:1]
        )
    )
)

【讨论】:

  • .order_by('giver__created') 不太正确 - 它必须是 ordered_by 那种最后的赞美......
  • @MichealJ.Roberts:但这正在发生。因为您也将related_name 设置为'giver'(这是从UserCompliment 的关系名称,所以是“反向”中的关系)。因此,它使用Complimentcreated。但我同意related_name 可以改进。
  • 太棒了 - 我明天会详细测试它。我已经下班了,但看起来很有希望。
  • 您好 Willem,非常感谢您的深思熟虑的回答。我保持相关名称相同,即给予者和接收者,因为这可能对我的代码库进行了重大更改。话虽如此,稍作调整....order_by('-giver__created')[:1] ...给我的不是Compliment的最后一个人,而是最新的,给了我想要的东西。我会说这个微小的变化更多的是我的解释。
  • @MichealJ.Roberts:可能通过将values('name') 替换为values(full_name=Concat('first_name', Value(' '), 'last_name'))(使用from django.db.models.functions import Concatfrom django.db.models import Value)。
猜你喜欢
  • 1970-01-01
  • 2012-03-07
  • 1970-01-01
  • 2020-09-09
  • 2015-08-25
  • 1970-01-01
  • 2018-11-03
  • 2017-03-03
  • 2015-04-07
相关资源
最近更新 更多