【问题标题】:How to get a Count based on a subquery?如何根据子查询获取计数?
【发布时间】:2020-05-08 11:05:37
【问题描述】:

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

我有四个模型:

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 and Sum annotations interfere with 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 显然不能/不愿意处理相关子查询,因为这些子查询被认为是次优的。

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

有人能帮我找出一种方法来为 MariaDB 实现这些辅助函数吗?

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

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

【问题讨论】:

  • @Bhargav Rao :我提供的是一种适用于多种环境的解决方案。我发现您在没有评论或解释的情况下删除它有点粗糙。由于我缺少 9 个学分来发表评论,因此发布部分答案以使事情顺利进行对我来说有点困难。感谢您的理解
  • 您可以从答案中编辑出后续问题,使其成为独立的解决方案并标记为取消删除。
  • 当你说“它崩溃了”时,你的意思是它崩溃了 MariaDB,还是返回了一个错误?如果是这样,错误是什么?如果我们能确切地看到问题出在哪里,这可能有助于理解问题。
  • @MatthewSchinckel:抱歉滥用语言;它在查询(226._mysql.connection.query(self,查询) - 在 MariaDB 文档中查看它,我发现 MariaDB 不支持“相关子查询”(mariadb.com/kb/en/subquery-limitations)

标签: django django-queryset correlated-subquery


【解决方案1】:

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

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

【讨论】:

  • 生成的 SQL 是什么?这与更传统的数据库生成的 SQL 相比如何?有什么我们可以添加到 Django 的子查询的东西来生成更好的相关子查询吗?
  • (此外,它生成的 SQL 可以说明模板的外观,简化您的代码以不需要包装)。
  • 看看您的解决方案,您应该能够直接在子查询注释中使用 Coalesce(...) 东西吗?
  • @MatthewSchinckel:谢谢。我可能会在本周末进一步调查。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-04
  • 2019-07-05
  • 2015-10-31
  • 2012-06-16
  • 2019-11-22
相关资源
最近更新 更多