Select 절에 inner 쿼리 형태를 만들고 inner 쿼리의 결과로 count를 세고자 할 때 아래와 같은 쿼리를 짤 수 있습니다.

SELECT 
    id,
    title,
    (SELECT COUNT(tag.id) AS count 
            FROM tag  
            INNER JOIN post ON (post.id = tag.id) 
            GROUP BY tag.id
    ) AS "count" 
FROM post


이러한 형태를 ORM으로 변형하면 다음과 같은 형태로 표현할 수 있습니다.

from django.db import models
from django.db.models import OuterRef, Subquery


class Tag(models.Model):
    name = models.CharField(max_length=120)

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




tag1 = Tag.objects.create(name='tag1')
post1 = Post.objects.create(title='post1')
post1.tags.add(tag1)

tags_list = Tag.objects.filter(post=OuterRef('pk'))
Post.objects.annotate(count=Subquery(tags_list.count()))
# 오류 발생
# ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.


Subquery() 내부에 count() 함수를 사용해 구현을 하면 위와 같은 오류가 발생합니다. OuterRef() 함수는 Subquery() 함수 내에서만 사용이 가능하므로 .count() 메소드를 사용할 수 없습니다. 이러한 문제를 해결하기 위해 Count() 함수를 사용해야 합니다.

from django.db.models import OuterRef, Subquery, Count


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


print(queryset.query)


# 아래 쿼리 또한 원하는 형태가 아님


# 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() 함수는 aggregate 이므로 select 절로 나오는 컬럼들을 자동으로 group by에 추가하게 됩니다. 이 문제를 해결하기 위해 values() + annotate() + values()를 사용해야 합니다.

from django.db.models import OuterRef, Subquery, Count




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


print(queryset.query)


# 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"


첫 번째 values()를 누락하면 select 절의 inner 쿼리의 group by에 더 많은 컬럼이 추가될 수 있습니다. 따라서 group by로 묶고자 하는 컬럼을 반드시 명시해야 합니다.

  1. 2020.08.06 09:26

    비밀댓글입니다