Производительность Django (серия 4 частей)
Сегодня мы собираемся обсудить Группа по агрегации (сумма)
Предположим, у нас есть столы пользователей, плейлистов и песен.
- Каждый пользователь может иметь один или несколько списков воспроизведения.
- Каждый плейлист может иметь одну или несколько песен
# 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'))
Так Если наша цель состоит в том, чтобы перечислить всех пользователей с помощью playlists_total_length
Насколько плохо этот код?
Если бы у нас было, например:
- 10 пользователей
- 10 плейлистов на пользователя
- 10 песен на плейлист
Общее количество объектов (пользователей) + 10*10 (плейлисты) + 10*10*10 (песни)
1110 дБ строки в 3 разных таблицах всего для 10 пользователей
Решение: группа по агрегации (сумма)
- Используйте агрегация Рассчитать общую длину всех песен:
Song.objects.aggregate (sum ('длина'))
SELECT SUM("song"."length") AS "length__sum" FROM "song";
Результат: {'length__sum': 490}
- Используйте аннотат Для группы по:
Song.objects.annotate (sum ('длина'))
SELECT * , SUM("song"."length") AS "length__sum" FROM "song" GROUP BY "song"."id", "song"."playlist_id", "song"."title", "song"."length
Результат:
- Используйте Значения Прежде чем аннотировать группу по конкретным столбцам:
Song.objects.values ('playlist_id'). Annotate (sum ('длина'))
SELECT "song"."playlist_id", SUM("song"."length") AS "length__sum" FROM "song" GROUP BY "song"."playlist_id"
Результат:
- Используйте values_list Чтобы выбрать значения суммы только не диктат
Song.objects.values (‘playlist_id’). Annotate (sum (‘length’)). Values_list (‘length__sum’)
Мы можем сжать этот запрос и все же получить тот же сгенерированный запрос SQL:
Song.objects.values (‘playlist_id’). Values_list (sum (‘length’))
SELECT SUM("song"."length") AS "length__sum" FROM "song" GROUP BY "song"."playlist_id"
Результат:
Перечислите все плейлисты с общей длиной песен:
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)
Overtref Позвонит получить доступ к полям от родительского запроса, когда вы находитесь в подразделении. OverTref (‘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')
Спасибо, Иво Дончев
Производительность Django (серия 4 частей)
Оригинал: “https://dev.to/shawara/django-orm-optimization-tips-3-aggregation-2bfi”