Оптимизация 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 theUser
model [Django-doc] directly. For more information you can see the referencing theUser
model section of the documentation [Django-doc].