首页 > 解决方案 > 如何根据子查询获取计数?

问题描述

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

我有四个模型:

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))

谁能解释我如何获得这两个值?并最终向我提出一个链接,该链接清楚地解释了如何将子查询用于比官方文档中不太明显的情况

提前致谢


编辑 2021 年 3 月:我最近发现了两篇文章,它们指导我完成了针对此特定问题的一个潜在解决方案: Django Count 和 Sum annotationsinterference of each otherDjango 1.11 Annotating a Subquery Aggregate

我从https://stackoverflow.com/users/188/matthew-schinckelhttps://stackoverflow.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 显然不能/不愿意处理相关子查询,因为这些子查询被认为是次优的。

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

谁能帮我想出一种方法来为 MariaDB 实现这些辅助函数?

这个环境应该是什么template

马修-辛克尔?贝努瓦布兰琼?克塔维?

标签: djangodjango-querysetcorrelated-subquery

解决方案


再深入一点并更详细地分析 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

我花了这么多时间来解决这个问题......我希望这可以避免你们中的许多人在寻找正确的方法时遇到的所有挫败感。


推荐阅读