Оптимизация Django QuerySet с помощью вложенных агрегаций

Я работаю над оптимизацией сложного Django-запроса, в котором мне нужно выполнить вложенные агрегации и условные аннотации для нескольких связанных моделей. Я хочу получить топ-5 самых активных пользователей на основе их взаимодействия с постами, а также рассчитать различные типы показателей вовлеченности (например, просмотры, комментарии и лайки).

Мои модели:

class User(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    created_at = models.DateTimeField()

class Engagement(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    type = models.CharField(max_length=50)  # 'view', 'like', 'comment'
    created_at = models.DateTimeField()

Вот как выглядит мой код:

from django.db.models import Count, Q

some_date = ...

top_users = (
    User.objects.annotate(
        view_count=Count('engagement__id', filter=Q(engagement__type='view', engagement__created_at__gte=some_date)),
        like_count=Count('engagement__id', filter=Q(engagement__type='like', engagement__created_at__gte=some_date)),
        comment_count=Count('engagement__id', filter=Q(engagement__type='comment', engagement__created_at__gte=some_date)),
        total_engagements=Count('engagement__id', filter=Q(engagement__created_at__gte=some_date))
    )
    .order_by('-total_engagements')[:5]
)

Это работает, однако производительность запросов не идеальна. При больших наборах данных такой подход приводит к медленному выполнению запросов, и мне интересно, эффективно ли использование нескольких аннотаций Count с условиями filter.

Есть ли более оптимизированный способ написать этот запрос или какие-либо лучшие практики, которые я должен рассмотреть для повышения производительности, особенно при работе с большими объемами данных? Любые соображения или предложения были бы очень полезны!

Это работает, однако производительность запроса не идеальна. При больших наборах данных такой подход приводит к медленному выполнению запросов, и я задаюсь вопросом, эффективно ли использование нескольких аннотаций Count с условиями фильтрации.

It is not very efficient. The filter=… [Django-doc] approach is implemented as a CASE … WHEN …, so that typically means the database will first consider all engagements, and then filter out the ones that do not satisfy the filter in a linear scan.

Если же мы никогда не хотим возвращать пользователей, не проявивших активности после some_date, мы можем повысить эффективность, фильтруя по JOIN:

top_users = (
    User.objects.filter(engagement__created_at__gte=some_date)
    .annotate(
        view_count=Count('engagement__id', filter=Q(engagement__type='view')),
        like_count=Count('engagement__id', filter=Q(engagement__type='like')),
        comment_count=Count(
            'engagement__id', filter=Q(engagement__type='comment')
        ),
        total_engagements=Count('engagement__id'),
    )
    .order_by('-total_engagements')[:5]
)

и добавьте db_index=True [Django-doc] на поле created_at:

class Engagement(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    type = models.CharField(max_length=50)  # 'view', 'like', 'comment'
    created_at = models.DateTimeField(db_index=True)

Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation [Django-doc].

Вернуться на верх