带OuterR的简单子查询

2024-04-28 12:04:01 发布

您现在位置:Python中文网/ 问答频道 /正文

我试图创建一个非常简单的子查询,它使用OuterRef(不是为了实际目的,只是为了让它工作),但仍然会遇到相同的错误。

柱/模型.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外壳代码

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

Tags: namefrompyimportselfobjectstitlemodels
2条回答

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

因此,人们可能认为正确的方法是使用Count()。可能是这样的:

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

这不起作用有两个原因:

  1. 查询集Tag选择所有Tag字段,而Count只能在一个字段上计数。因此:Tag.objects.filter(post=OuterRef('pk')).only('pk')是必需的(选择依赖于tag.pk)。

  2. Count本身不是一个Subquery类,Count是一个Aggregate。因此,由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"

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中生成一个子选择,而不是加入相关表。

相关问题 更多 >