Django ORM 최적화 팁 #3 집계

오늘 우리는 GROUP BY와 집계(SUM)에 대해 논의할 것입니다.

사용자 테이블, 재생 목록 및 노래가 있다고 가정합니다.
  • 각 사용자는 하나 이상의 재생 목록을 가질 수 있습니다.
  • 각 재생 목록에는 하나 이상의 노래가 포함될 수 있습니다.

  • # models
    class User(models.Model):
        name = models.CharField(max_length=127)
    
        @property
        def playlists_total_length(self) -> int:
            return sum([p.songs_total_length for p in self.playlists.all()])
    
    
    class Playlist(models.Model):
        user = models.ForeignKey(User, related_name='playlists')
        name = models.CharField(max_length=255)
    
        @property
        def songs_total_length(self) -> int:
            return sum([song.length for song in self.songs.all()])
    
    
    class Song(models.Model):
        playlist = models.ForeignKey(Playlist, related_name='songs')
        title = models.CharField(max_length=255)
        length = models.PositiveIntegerField(help_text=_('In seconds'))
    


    따라서 우리의 목표가 재생 목록이 있는 모든 사용자를 나열하는 것이라면

    이 코드가 얼마나 나쁜가요?



    예를 들면 다음과 같습니다.
  • 사용자 10명
  • 사용자당 10개의 재생 목록
  • 재생 목록당 10곡

  • 가져올 총 개체 = 10(사용자) + 10*10(재생 목록) + 10*10*10(노래)

    단 10명의 사용자를 위한 3개의 다른 테이블에 1110개의 DB 행

    솔루션: 집계(SUM)가 있는 GROUP BY



  • aggregation을 사용하여 모든 노래의 총 길이를 계산합니다.Song.objects.aggregate(Sum('length'))

  • SELECT SUM("song"."length") AS "length__sum" FROM "song";
    


    결과: {'length__sum': 490}

  • 그룹화 기준annotate 사용:Song.objects.annotate(Sum('length'))

  • SELECT * , SUM("song"."length") AS "length__sum" 
    FROM "song" 
    GROUP BY "song"."id", 
             "song"."playlist_id", 
             "song"."title",
             "song"."length
    


    결과: <QuerySet [Song(...),Song(...),...]>

  • 특정 열을 기준으로 그룹화하려면 주석 앞에 values을 사용하십시오.Song.objects.values('playlist_id').annotate(Sum('length'))

  • SELECT "song"."playlist_id", SUM("song"."length") AS "length__sum"
    FROM "song" 
    GROUP BY "song"."playlist_id"
    


    결과: <QuerySet [{'playlist_id': 2, 'length__sum': 310}, ...]>

  • values_list을 사용하여 dict
  • 가 아닌 합계 값만 선택합니다.
    Song.objects.values('playlist_id').annotate(Sum('length')).values_list('length__sum')
    We can compress this query and still getting the same generated sql query:

    Song.objects.values('playlist_id').values_list(Sum('length'))



    SELECT SUM("song"."length") AS "length__sum" 
    FROM "song" 
    GROUP BY "song"."playlist_id"
    


    결과: <QuerySet [(310,), (180,)]>

    총 노래 길이가 있는 모든 재생 목록 나열:




    songs_subquery = Subquery(
        queryset=Song.objects
            .values('playlist_id') \
            .filter(playlist__id=OuterRef('id')) \
            .values_list(Sum('length')),
        output_field=IntegerField())
    
    queryset = Playlist.objects.annotate(
        _songs_total_length=songs_subquery)
    


    OuterRef을 사용하면 하위 쿼리에 있을 때 상위 쿼리의 필드에 액세스할 수 있습니다. OuterRef('id') -> "playlist"."id"
    SELECT "playlist"."id",
           "playlist"."user_id",
           "playlist"."name",
           (
               SELECT SUM("song"."length")
                   FROM "song"
                   WHERE "song"."playlist_id" = ("playlist"."id")
                   GROUP BY "song"."playlist_id" LIMIT 1
            ) AS "songs_total_length"
        FROM "playlist";
    


    총 노래 길이가 있는 모든 사용자 나열:




    songs_subquery = Subquery(
        queryset=Song.objects
            .values('playlist__id') \
            .filter(playlist__id=OuterRef('id')) \
            .values_list(Sum('length')),
        output_field=IntegerField(default=0))
    
    playlists_subquery = Subquery(
        queryset=Playlist.objects
            .values('user__id') \
            .filter(user__id=OuterRef('id')) \
            .annotate(_songs_total_length=Sum(songs_subquery)) \
            .values_list('_songs_total_length'),
        output_field=IntegerField(default=0))
    
    queryset = User.objects.annotate(_playlists_total_length=playlists_subquery)
    



    SELECT "user"."id",
           "user"."name",
           (SELECT SUM((SELECT SUM(U0."length") AS "length__sum"
                        FROM "song" U0
                        WHERE U0."playlist_id" = V0."id"
                        GROUP BY U0."playlist_id")) AS "_songs_total_length"
            FROM "playlist" V0
            WHERE V0."user_id" = "user"."id"
            GROUP BY V0."user_id") AS "_playlists_total_length"
    FROM "user";
    



    원하는 모든 곳에 이 복잡한 쿼리를 작성하는 것은 매우 어려울 것입니다.



    최종 클린 코드는 다음과 같습니다.




    class UserQuerySet(models.QuerySet):
        @classmethod
        def playlists_total_length(cls):
            playlist_annotation = PlaylistQuerySet.songs_total_length()
    
            queryset = Playlist.objects \
                .values('user__id') \
                .filter(user__id=OuterRef('id')) \
                .values_list(Sum(playlist_annotation))
    
            return Subquery(
                queryset=queryset,
                output_field=IntegerField()
            )
    
        def collect(self):
            return self.annotate(_playlists_total_length=self.playlists_total_length())
    
    
    class User(models.Model):
        objects = UserQuerySet.as_manager()
        name = models.CharField(max_length=255)
    
        @property
        def playlists_total_length(self):
            if hasattr(self, '_playlists_total_length'):
                return self._playlists_total_length
            return sum([p.songs_total_length for p in self.playlists.all()])
    
    
    
    class PlaylistQuerySet(models.QuerySet):
        @classmethod
        def songs_total_length(cls):
            queryset = Song.objects \
                .values('playlist__id') \
                .filter(playlist__id=OuterRef('id')) \
                .values_list(Sum('length'))  # The SUM aggregation
    
            return Subquery(
                queryset=queryset,
                output_field=models.IntegerField()
            )
    
        def collect(self):
            return self.annotate(_songs_total_length=self.songs_total_length())
    
    
    class Playlist(models.Model):
        objects = PlaylistQuerySet.as_manager()
        user = models.ForeignKey(User, related_name='playlists')
        name = models.CharField(max_length=255)
    
        @property
        def songs_total_length(self):
            if hasattr(self, '_songs_total_length'):
                return self._songs_total_length
            return sum([song.length for song in self.songs.all()])
    
    
    
    class Song(models.Model):
        playlist = models.ForeignKey(Playlist, related_name='songs')
        title = models.CharField(max_length=255)
        length = models.PositiveIntegerField(help_text='In seconds')
    



    감사합니다, Ivo Donchev

    좋은 웹페이지 즐겨찾기