带有 OuterRef 的简单子查询

Posted

技术标签:

【中文标题】带有 OuterRef 的简单子查询【英文标题】:Simple Subquery with OuterRef 【发布时间】:2017-10-01 21:06:50 【问题描述】:

我正在尝试制作一个使用OuterRef 的非常简单的Subquery(不是出于实际目的,只是为了让它工作),但我一直遇到同样的错误。

posts/models.py代码

from django.db import models

class Tag(models.Model):
    name = models.CharField(max_length=120)
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=120)
    tags = models.ManyToManyField(Tag)
    def __str__(self):
        return self.title

manage.py shell代码

>>> from django.db.models import OuterRef, Subquery
>>> from posts.models import Tag, Post
>>> tag1 = Tag.objects.create(name='tag1')
>>> post1 = Post.objects.create(title='post1')
>>> post1.tags.add(tag1)
>>> Tag.objects.filter(post=post1.pk)
<QuerySet [<Tag: tag1>]>
>>> tags_list = Tag.objects.filter(post=OuterRef('pk'))
>>> Post.objects.annotate(count=Subquery(tags_list.count()))

最后两行应该给我每个 Post 对象的标签数量。在这里我不断收到同样的错误:

ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

【问题讨论】:

【参考方案1】:

您的示例的一个问题是您不能将queryset.count() 用作子查询,因为.count() 会尝试评估查询集并返回计数。

所以人们可能认为正确的方法是使用Count()。也许是这样的:

Post.objects.annotate(
    count=Count(Tag.objects.filter(post=OuterRef('pk')))
)

这不起作用有两个原因:

    Tag 查询集选择所有Tag 字段,而Count 只能依靠一个字段。因此:需要Tag.objects.filter(post=OuterRef('pk')).only('pk')(选择依靠tag.pk)。

    Count 本身不是Subquery 类,CountAggregate。所以Count生成的表达式不能被识别为SubqueryOuterRef需要子查询),我们可以通过使用Subquery来解决这个问题。

对 1) 和 2) 应用修复会产生:

Post.objects.annotate(
    count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)

但是 如果您检查正在生成的查询:

SELECT 
    "tests_post"."id",
    "tests_post"."title",
    COUNT((SELECT U0."id" 
            FROM "tests_tag" U0 
            INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") 
            WHERE U1."post_id" = ("tests_post"."id"))
    ) AS "count" 
FROM "tests_post" 
GROUP BY 
    "tests_post"."id",
    "tests_post"."title"

您会注意到GROUP BY 子句。这是因为COUNT 是一个聚合函数。现在它不会影响结果,但在其他一些情况下可能会。这就是为什么docs 提出了一种不同的方法,通过values + annotate + values 的特定组合将聚合移动到subquery

Post.objects.annotate(
    count=Subquery(
        Tag.objects
            .filter(post=OuterRef('pk'))
            # The first .values call defines our GROUP BY clause
            # Its important to have a filtration on every field defined here
            # Otherwise you will have more than one group per row!!!
            # This will lead to subqueries to return more than one row!
            # But they are not allowed to do that!
            # In our example we group only by post
            # and we filter by post via OuterRef
            .values('post')
            # Here we say: count how many rows we have per group 
            .annotate(count=Count('pk'))
            # Here we say: return only the count
            .values('count')
    )
)

最后会产生:

SELECT 
    "tests_post"."id",
    "tests_post"."title",
    (SELECT COUNT(U0."id") AS "count" 
            FROM "tests_tag" U0 
            INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") 
            WHERE U1."post_id" = ("tests_post"."id") 
            GROUP BY U1."post_id"
    ) AS "count" 
FROM "tests_post"

【讨论】:

谢谢,成功了!但是,当我将pk__in=[1,2] 添加到标签过滤器时,我得到django.core.exceptions.FieldError: Expression contains mixed types. You must set output_field 您可以尝试打印queryset.query 并直接在您的RDBMS 中执行它以查看您得到的回报。我猜想对于某些行Count 可能会返回NULL 而不是0。您可以尝试通过临时排除不带计数的行来确认这一点,即.filter(count__gte=1)。但是,Subquery 接受第二个参数,即output_field,您可以尝试将其设置为:output_field=fields.IntegerField() @Todor 对我不起作用。我得到 django.db.utils.ProgrammingError: more than one row returned by a subquery used as an expression 我猜你缺少一些过滤器,所以每行有多个组返回多个计数。您必须对子查询中的第一个 .values() 子句中的每个字段都有一个过滤器。 在 Tag 上设置默认排序将使 GROUP BY 还包括排序中定义的字段以及 values() 中的字段,这会生成 Subquery returns more than 1 row 错误。通过添加 ).order_by('post').values('post') 来修复【参考方案2】:

django-sql-utils 包使这种子查询聚合变得简单。只需pip install django-sql-utils 然后:

from sql_util.utils import SubqueryCount
posts = Post.objects.annotate(
            tag_count=SubqueryCount('tag'))

SubqueryCount 的 API 与 Count 相同,但它在 SQL 中生成一个子选择,而不是连接到相关表。

【讨论】:

以上是关于带有 OuterRef 的简单子查询的主要内容,如果未能解决你的问题,请参考以下文章

获取子查询的计数

带有子查询的休眠选择查询?

带有子查询的 MySQL UPDATE 查询永远存在

带有重复子查询的mySQL select联合查询工作缓慢

当 SQLAlchemy 决定使用带有 .limit() 方法的子查询时?

带有子查询的 MySQL UPDATE TABLE 不会先执行子查询