ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] OuterRef, Subquery 사용할 때, .count() 오류 해결법
    언어/파이썬 & 장고 2020. 4. 27. 20:02

    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로 묶고자 하는 컬럼을 반드시 명시해야 합니다.

    댓글