在Django查询中对使用.extra(select={...})引入的值使用.aggregate()?

23 投票
2 回答
10289 浏览
提问于 2025-04-16 09:14

我想统计一个玩家每周玩了多少次,像这样:

player.game_objects.extra(
    select={'week': 'WEEK(`games_game`.`date`)'}
).aggregate(count=Count('week'))

但是Django报错了,提示我有问题:

FieldError: Cannot resolve keyword 'week' into field. Choices are: <lists model fields>

我可以用原始SQL这样做:

SELECT WEEK(date) as week, COUNT(WEEK(date)) as count FROM games_game
WHERE player_id = 3
GROUP BY week

有没有什么好的方法可以在Django中做到这一点,而不需要直接写原始SQL呢?

2 个回答

4

这里有一个问题的例子和一个不太理想的解决方法。我们来看这个模型的例子:

class Rating(models.Model):
    RATING_CHOICES = (
        (1, '1'),
        (2, '2'),
        (3, '3'),
        (4, '4'),
        (5, '5'),
    )
    rating = models.PositiveIntegerField(choices=RATING_CHOICES)
    rater = models.ForeignKey('User', related_name='ratings_given')
    ratee = models.ForeignKey('User', related_name='ratings_received')

这个聚合查询和你的情况一样失败,因为它试图引用一个用 .extra() 创建的非字段值。

User.ratings_received.extra(
    select={'percent_positive': 'ratings > 3'}
).aggregate(count=Avg('positive'))

一个解决方法

我们可以通过在额外值的定义中直接使用聚合数据库函数(在这个例子中是平均值 Avg)来找到想要的值:

User.ratings.extra(
    select={'percent_positive': 'AVG(rating >= 3)'}
)

这个查询会生成以下的 SQL 查询:

SELECT (AVG(rating >= 3)) AS `percent_positive`,
       `ratings_rating`.`id`,
       `ratings_rating`.`rating`,
       `ratings_rating`.`rater_id`,
       `ratings_rating`.`ratee_id`
FROM `ratings_rating`
WHERE `ratings_rating`.`ratee_id` = 1

尽管这个查询中有一些不必要的列,我们仍然可以通过提取 percent_positive 的值来获得我们想要的结果:

User.ratings.extra(
    select={'percent_positive': 'AVG(rating >= 3)'}
).values('percent_positive')[0]['percent_positive']
15

你可以使用一个自定义的聚合函数来生成你的查询:

WEEK_FUNC = 'STRFTIME("%%%%W", %s)' # use 'WEEK(%s)' for mysql

class WeekCountAggregate(models.sql.aggregates.Aggregate):
    is_ordinal = True
    sql_function = 'WEEK' # unused
    sql_template = "COUNT(%s)" % (WEEK_FUNC.replace('%%', '%%%%') % '%(field)s')

class WeekCount(models.aggregates.Aggregate):
    name = 'Week'
    def add_to_query(self, query, alias, col, source, is_summary):
        query.aggregates[alias] = WeekCountAggregate(col, source=source, 
            is_summary=is_summary, **self.extra)


>>> game_objects.extra(select={'week': WEEK_FUNC % '"games_game"."date"'}).values('week').annotate(count=WeekCount('pk'))

不过,因为这个接口没有文档说明,而且已经需要一些原始的SQL代码,可能使用原始查询会更好。

撰写回答