如何根据子查询获取计数?

How to get a Count based on a subquery?

尽管我一直在基于我的网络搜索进行尝试,但我仍然很难让查询正常工作,我认为在发疯之前我需要一些帮助。

我有四个模型:

class Series(models.Model):
    puzzles = models.ManyToManyField(Puzzle, through='SeriesElement', related_name='series')
    ...

class Puzzle(models.Model):
    puzzles = models.ManyToManyField(Puzzle, through='SeriesElement', related_name='series')
    ...

class SeriesElement(models.Model):
    puzzle = models.ForeignKey(Puzzle,on_delete=models.CASCADE,verbose_name='Puzzle',)
    series = models.ForeignKey(Series,on_delete=models.CASCADE,verbose_name='Series',)
    puzzle_index = models.PositiveIntegerField(verbose_name='Order',default=0,editable=True,)

class Play(models.Model):
    puzzle = models.ForeignKey(Puzzle, on_delete=models.CASCADE, related_name='plays')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True,null=True, on_delete=models.SET_NULL, related_name='plays')
    series = models.ForeignKey(Series, blank=True, null=True, on_delete=models.SET_NULL, related_name='plays')
    puzzle_completed = models.BooleanField(default=None, blank=False, null=False)
    ...

每个用户可以多次玩任何谜题,每次创建一个 Play 记录。 这意味着对于一组给定的 (user,series,puzzle) 我们可以有几个 Play 记录, 有些 puzzle_completed = True,有些 puzzle_completed = False

我正在尝试(未成功)实现的是通过注释为每个系列计算拼图 nb_completed_by_usernb_not_completed_by_user.

的数量

对于 nb_completed_by_user,我有一些在几乎所有情况下都有效的东西(我的一项测试中有一个我目前无法解释的故障):

Series.objects.annotate(nb_completed_by_user=Count('puzzles',
filter=Q(puzzles__plays__puzzle_completed=True, 
    puzzles__plays__series_id=F('id'),puzzles__plays__user=user), distinct=True))

对于 nb_not_completed_by_user,我能够对 Puzzle 进行查询,这给了我很好的答案,但我无法将其转换为有效的 Subquery 表达式不会抛出错误,或得到 Count 表达式来给我正确的答案。

这个有效:

puzzles = Puzzle.objects.filter(~Q(plays__puzzle_completed=True,
 plays__series_id=1, plays__user=user),series=s)

但是当尝试移动到子查询时,我找不到使用以下表达式不抛出错误的方法:ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

pzl_completed_by_user = Puzzle.objects.filter(plays__series_id=OuterRef('id')).exclude(
    plays__puzzle_completed=True,plays__series_id=OuterRef('id'), plays__user=user)

和以下 Count 表达式没有给我正确的结果:

Series.objects.annotate(nb_not_completed_by_user=Count('puzzles', filter=~Q(
            puzzle__plays__puzzle_completed=True, puzzle__plays__series_id=F('id'), 
            puzzle__plays__user=user))

任何人都可以向我解释如何获得这两个值吗? 并最终向我推荐一个 link ,它清楚地解释了如何在比官方文档

中更不明显的情况下使用子查询

提前致谢


编辑 2021 年 3 月: 我最近发现了两个帖子,它们指导我完成了针对这个特定问题的一个潜在解决方案:

我实施了 https://whosebug.com/users/188/matthew-schinckel and https://whosebug.com/users/1164966/benoit-blanchon 提出的解决方案 有帮助 类: class SubqueryCount(Subquery)class SubquerySum(Subquery)

class SubqueryCount(Subquery):
    template = "(SELECT count(*) FROM (%(subquery)s) _count)"
    output_field = PositiveIntegerField()


class SubquerySum(Subquery):
    template = '(SELECT sum(_sum."%(column)s") FROM (%(subquery)s) _sum)'

    def __init__(self, queryset, column, output_field=None, **extra):
        if output_field is None:
            output_field = queryset.model._meta.get_field(column)
        super().__init__(queryset, output_field, column=column, **extra)

效果非常好!并且比传统的 Django Count 注释快得多。 ...至少在 SQlite 中,可能还有其他人所说的 PostgreSQL。

但是当我在 MariaDB 环境中尝试时...它崩溃了! MariaDB 显然不能/不愿意处理相关子查询,因为这些子查询被认为是次优的。

就我而言,当我尝试同时从数据库中为每条记录获取多个 Count/distinct 注释时,我确实看到了性能的巨大提升(在 SQLite 中) 我想在 MariaDB 中复制。

谁能帮我想办法为 MariaDB 实现这些辅助函数?

在这个环境中 template 应该是什么?

马修辛克尔? benoit-blanchon ? 克塔维?

更深入一点并更详细地分析 Django 文档,我终于能够产生一种令人满意的方法来生成基于子查询的 Count 或 Sum。

为了简化流程,我定义了以下辅助函数:

要生成子查询:

def get_subquery(app_label, model_name, reference_to_model_object, filter_parameters={}):
    """
    Return a subquery from a given model (work with both FK & M2M)
    can add extra filter parameters as dictionary:

    Use:
        subquery = get_subquery(
                    app_label='puzzles', model_name='Puzzle',
                    reference_to_model_object='puzzle_family__target'
                    )
        or directly:
        qs.annotate(nb_puzzles=subquery_count(get_subquery(
            'puzzles', 'Puzzle','puzzle_family__target')),)
    """
    model = apps.get_model(app_label, model_name)

    # we need to declare a local dictionary to prevent the external dictionary to be changed by the update method:
    parameters = {f'{reference_to_model_object}__id': OuterRef('id')}
    parameters.update(filter_parameters)
    # putting '__id' instead of '_id' to work with both FK & M2M
    return model.objects.filter(**parameters).order_by().values(f'{reference_to_model_object}__id')

统计通过get_subquery产生的子查询:

def subquery_count(subquery):
    """  
    Use:
        qs.annotate(nb_puzzles=subquery_count(get_subquery(
            'puzzles', 'Puzzle','puzzle_family__target')),)
    """
    return Coalesce(Subquery(subquery.annotate(count=Count('pk', distinct=True)).order_by().values('count'), output_field=PositiveIntegerField()), 0)

对通过get_subquery生成的子查询求和字段field_to_sum:

def subquery_sum(subquery, field_to_sum, output_field=None):
    """  
    Use:
        qs.annotate(total_points=subquery_sum(get_subquery(
            'puzzles', 'Puzzle','puzzle_family__target'),'points'),)
    """
    if output_field is None:
        output_field = queryset.model._meta.get_field(column)

    return Coalesce(Subquery(subquery.annotate(result=Sum(field_to_sum, output_field=output_field)).order_by().values('result'), output_field=output_field), 0)

所需的导入:

from django.db.models import Count, Subquery, PositiveIntegerField, DecimalField, Sum
from django.db.models.functions import Coalesce

我花了很多时间来解决这个... 我希望这可以让你们中的许多人免去我在寻找正确的前进方式时所经历的所有挫折。