Рубрики
Без рубрики

Django Orm Optimization Советы № 3 агрегация

Сегодня мы собираемся обсудить группу с агрегацией (сумма) Предположим, у нас есть столы пользователей, Playl … Tagged с Python, Django, базой данных, SQL.

Производительность 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”