Django ORM 최적화 팁 #3 집계
사용자 테이블, 재생 목록 및 노래가 있다고 가정합니다.
# 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명의 사용자를 위한 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
Reference
이 문제에 관하여(Django ORM 최적화 팁 #3 집계), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/shawara/django-orm-optimization-tips-3-aggregation-2bfi텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)