Агрегация в Django ORM

Агрегация позволяет выполнять вычисления над группами объектов. Это мощный инструмент для анализа данных, создания отчетов и оптимизации запросов.

Основные функции агрегации

Django предоставляет несколько встроенных функций агрегации:

  • Count - подсчет количества объектов
  • Sum - сумма значений
  • Avg - среднее значение
  • Min - минимальное значение
  • Max - максимальное значение
  • StdDev - стандартное отклонение
  • Variance - дисперсия

Базовые примеры агрегации

 1from django.db.models import Count, Sum, Avg, Min, Max
 2from django.db.models.functions import Coalesce
 3from .models import Book, Author, Category, Order, Product
 4
 5# Подсчет общего количества книг
 6total_books = Book.objects.count()
 7
 8# Подсчет книг по авторам
 9authors = Author.objects.annotate(
10    books_count=Count('books')
11).filter(books_count__gt=0)
12
13# Средняя оценка книг
14avg_rating = Book.objects.aggregate(
15    avg_rating=Avg('rating')
16)['avg_rating']
17
18# Статистика по ценам книг
19price_stats = Book.objects.aggregate(
20    min_price=Min('price'),
21    max_price=Max('price'),
22    avg_price=Avg('price'),
23    total_books=Count('id')
24)
25
26# Общая стоимость всех книг
27total_value = Book.objects.aggregate(
28    total_value=Sum('price')
29)['total_value']
30
31# Количество книг по категориям
32category_stats = Category.objects.annotate(
33    book_count=Count('books'),
34    avg_price=Avg('books__price'),
35    total_value=Sum('books__price')
36).filter(book_count__gt=0)

Аннотации с вычисляемыми полями

 1from django.db.models import F, Q, Case, When, Value
 2from django.db.models.functions import ExtractYear, ExtractMonth
 3
 4class BookViewSet(viewsets.ModelViewSet):
 5    def get_queryset(self):
 6        """Queryset с аннотациями для аналитики"""
 7        return Book.objects.annotate(
 8            # Количество отзывов
 9            review_count=Count('reviews'),
10
11            # Средний рейтинг (с обработкой None)
12            avg_rating=Coalesce(Avg('reviews__rating'), 0.0),
13
14            # Год публикации
15            publication_year=ExtractYear('published_date'),
16
17            # Месяц публикации
18            publication_month=ExtractMonth('published_date'),
19
20            # Статус популярности
21            popularity_status=Case(
22                When(review_count__gte=100, then=Value('Очень популярная')),
23                When(review_count__gte=50, then=Value('Популярная')),
24                When(review_count__gte=10, then=Value('Средняя популярность')),
25                default=Value('Малоизвестная'),
26                output_field=models.CharField(),
27            ),
28
29            # Прибыльность (если есть данные о продажах)
30            profitability=Case(
31                When(price__gte=1000, then=Value('Высокая')),
32                When(price__gte=500, then=Value('Средняя')),
33                default=Value('Низкая'),
34                output_field=models.CharField(),
35            ),
36
37            # Скидка (если есть)
38            discount_amount=F('price') * F('discount_percent') / 100,
39            final_price=F('price') - F('discount_amount'),
40
41            # Возраст книги в годах
42            book_age=ExtractYear('published_date') - 2024,
43        ).select_related('author', 'category').prefetch_related('reviews')
44
45# Аннотации для пользователей
46user_stats = User.objects.annotate(
47    # Количество заказов
48    order_count=Count('orders'),
49
50    # Общая сумма заказов
51    total_spent=Sum('orders__total_amount'),
52
53    # Средний размер заказа
54    avg_order_value=Avg('orders__total_amount'),
55
56    # Последний заказ
57    last_order_date=Max('orders__created_at'),
58
59    # Статус клиента
60    customer_status=Case(
61        When(total_spent__gte=10000, then=Value('VIP')),
62        When(total_spent__gte=5000, then=Value('Постоянный')),
63        When(total_spent__gte=1000, then=Value('Активный')),
64        default=Value('Новый'),
65        output_field=models.CharField(),
66    )
67).filter(order_count__gt=0)

Сложные агрегации с группировкой

 1from django.db.models import Count, Sum, Avg, Q
 2from django.db.models.functions import TruncDate, TruncMonth, TruncYear
 3
 4# Анализ продаж по дням
 5daily_sales = Order.objects.annotate(
 6    date=TruncDate('created_at')
 7).values('date').annotate(
 8    order_count=Count('id'),
 9    total_revenue=Sum('total_amount'),
10    avg_order_value=Avg('total_amount'),
11    unique_customers=Count('user', distinct=True)
12).order_by('date')
13
14# Анализ продаж по месяцам
15monthly_sales = Order.objects.annotate(
16    month=TruncMonth('created_at')
17).values('month').annotate(
18    order_count=Count('id'),
19    total_revenue=Sum('total_amount'),
20    avg_order_value=Avg('total_amount'),
21    top_product=Subquery(
22        OrderItem.objects.filter(
23            order__created_at__month=OuterRef('month')
24        ).values('product__name').annotate(
25            total_sold=Sum('quantity')
26        ).order_by('-total_sold').values('product__name')[:1]
27    )
28).order_by('month')
29
30# Анализ продуктов по категориям
31product_analysis = Category.objects.annotate(
32    # Количество продуктов
33    product_count=Count('products'),
34
35    # Средняя цена
36    avg_price=Avg('products__price'),
37
38    # Общая стоимость
39    total_value=Sum('products__price'),
40
41    # Количество активных продуктов
42    active_products=Count(
43        'products',
44        filter=Q(products__is_active=True)
45    ),
46
47    # Продукты со скидкой
48    discounted_products=Count(
49        'products',
50        filter=Q(products__discount_percent__gt=0)
51    ),
52
53    # Средняя скидка
54    avg_discount=Avg(
55        'products__discount_percent',
56        filter=Q(products__discount_percent__gt=0)
57    )
58).filter(product_count__gt=0)
59
60# Анализ отзывов по продуктам
61review_analysis = Product.objects.annotate(
62    review_count=Count('reviews'),
63    avg_rating=Avg('reviews__rating'),
64    positive_reviews=Count(
65        'reviews',
66        filter=Q(reviews__rating__gte=4)
67    ),
68    negative_reviews=Count(
69        'reviews',
70        filter=Q(reviews__rating__lt=3)
71    ),
72    rating_distribution=Case(
73        When(avg_rating__gte=4.5, then=Value('Отлично')),
74        When(avg_rating__gte=4.0, then=Value('Хорошо')),
75        When(avg_rating__gte=3.0, then=Value('Удовлетворительно')),
76        default=Value('Плохо'),
77        output_field=models.CharField(),
78    )
79).filter(review_count__gt=0)

Агрегация с фильтрами и условиями

 1from django.db.models import Count, Sum, Avg, Q, F
 2from django.utils import timezone
 3from datetime import timedelta
 4
 5# Анализ продаж за последние 30 дней
 6thirty_days_ago = timezone.now() - timedelta(days=30)
 7
 8recent_sales = Order.objects.filter(
 9    created_at__gte=thirty_days_ago,
10    status='completed'
11).aggregate(
12    total_orders=Count('id'),
13    total_revenue=Sum('total_amount'),
14    avg_order_value=Avg('total_amount'),
15    unique_customers=Count('user', distinct=True)
16)
17
18# Анализ по статусам заказов
19order_status_analysis = Order.objects.values('status').annotate(
20    order_count=Count('id'),
21    total_revenue=Sum('total_amount'),
22    avg_order_value=Avg('total_amount'),
23    completion_rate=Case(
24        When(
25            status='completed',
26            then=Count('id') * 100.0 / Count('id', filter=Q(status__in=['pending', 'processing', 'completed']))
27        ),
28        default=Value(0),
29        output_field=models.FloatField(),
30    )
31).order_by('-order_count')
32
33# Анализ продуктов по ценовым категориям
34price_category_analysis = Product.objects.annotate(
35    price_category=Case(
36        When(price__lt=100, then=Value('Бюджетные')),
37        When(price__lt=500, then=Value('Средние')),
38        When(price__lt=1000, then=Value('Премиум')),
39        default=Value('Люкс'),
40        output_field=models.CharField(),
41    )
42).values('price_category').annotate(
43    product_count=Count('id'),
44    avg_price=Avg('price'),
45    total_value=Sum('price'),
46    active_products=Count('id', filter=Q(is_active=True)),
47    discounted_products=Count('id', filter=Q(discount_percent__gt=0))
48).order_by('avg_price')
49
50# Анализ пользователей по активности
51user_activity_analysis = User.objects.annotate(
52    # Последняя активность
53    last_activity=Max('orders__created_at'),
54
55    # Дни с момента последнего заказа
56    days_since_last_order=Case(
57        When(
58            last_activity__isnull=False,
59            then=ExtractDay(timezone.now() - F('last_activity'))
60        ),
61        default=Value(999),
62        output_field=models.IntegerField(),
63    ),
64
65    # Статус активности
66    activity_status=Case(
67        When(days_since_last_order__lte=30, then=Value('Активный')),
68        When(days_since_last_order__lte=90, then=Value('Умеренно активный')),
69        When(days_since_last_order__lte=365, then=Value('Неактивный')),
70        default=Value('Забытый'),
71        output_field=models.CharField(),
72    )
73).values('activity_status').annotate(
74    user_count=Count('id'),
75    avg_order_value=Avg('orders__total_amount'),
76    total_revenue=Sum('orders__total_amount')
77).order_by('user_count')

Оптимизация агрегационных запросов

 1from django.db.models import Count, Sum, Avg
 2from django.core.cache import cache
 3from django.db import connection
 4
 5class OptimizedAnalyticsViewSet(viewsets.ViewSet):
 6    """Оптимизированный ViewSet для аналитики"""
 7
 8    def get_sales_summary(self, request):
 9        """Кешированный отчет о продажах"""
10        cache_key = 'sales_summary'
11        result = cache.get(cache_key)
12
13        if result is None:
14            # Выполняем агрегацию
15            result = Order.objects.filter(
16                status='completed'
17            ).aggregate(
18                total_orders=Count('id'),
19                total_revenue=Sum('total_amount'),
20                avg_order_value=Avg('total_amount'),
21                unique_customers=Count('user', distinct=True)
22            )
23
24            # Кешируем на 1 час
25            cache.set(cache_key, result, 3600)
26
27        return Response(result)
28
29    def get_product_performance(self, request):
30        """Оптимизированный анализ продуктов"""
31        # Используем select_related для связанных объектов
32        queryset = Product.objects.select_related(
33            'category', 'brand'
34        ).annotate(
35            review_count=Count('reviews'),
36            avg_rating=Avg('reviews__rating'),
37            total_sales=Sum('order_items__quantity'),
38            revenue=Sum(F('order_items__quantity') * F('price'))
39        ).filter(
40            is_active=True
41        ).order_by('-revenue')
42
43        return Response(ProductSerializer(queryset, many=True).data)
44
45    def get_customer_segments(self, request):
46        """Сегментация клиентов с оптимизацией"""
47        # Используем только необходимые поля
48        queryset = User.objects.only(
49            'id', 'username', 'email', 'date_joined'
50        ).annotate(
51            order_count=Count('orders'),
52            total_spent=Sum('orders__total_amount'),
53            last_order=Max('orders__created_at')
54        ).filter(
55            order_count__gt=0
56        ).order_by('-total_spent')
57
58        return Response(UserSerializer(queryset, many=True).data)
59
60# Оптимизация с использованием raw SQL для сложных агрегаций
61def get_complex_analytics():
62    """Сложная аналитика с raw SQL"""
63    with connection.cursor() as cursor:
64        cursor.execute("""
65            SELECT
66                DATE_TRUNC('month', o.created_at) as month,
67                c.name as category,
68                COUNT(o.id) as order_count,
69                SUM(oi.quantity * p.price) as revenue,
70                AVG(oi.quantity * p.price) as avg_order_value
71            FROM orders o
72            JOIN order_items oi ON o.id = oi.order_id
73            JOIN products p ON oi.product_id = p.id
74            JOIN categories c ON p.category_id = c.id
75            WHERE o.status = 'completed'
76            GROUP BY DATE_TRUNC('month', o.created_at), c.name
77            ORDER BY month DESC, revenue DESC
78        """)
79
80        columns = [col[0] for col in cursor.description]
81        return [dict(zip(columns, row)) for row in cursor.fetchall()]
82
83# Использование индексов для ускорения агрегации
84class Product(models.Model):
85    name = models.CharField(max_length=200, db_index=True)
86    price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)
87    category = models.ForeignKey(Category, on_delete=models.CASCADE, db_index=True)
88    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
89
90    class Meta:
91        indexes = [
92            models.Index(fields=['category', 'price']),
93            models.Index(fields=['created_at', 'category']),
94            models.Index(fields=['is_active', 'price']),
95        ]

Агрегация для отчетов и дашбордов

  1from django.db.models import Count, Sum, Avg, Q
  2from django.db.models.functions import TruncDate, TruncMonth
  3from rest_framework.decorators import action
  4from rest_framework.response import Response
  5
  6class DashboardViewSet(viewsets.ViewSet):
  7    """ViewSet для дашборда с агрегацией"""
  8
  9    @action(detail=False, methods=['get'])
 10    def sales_overview(self, request):
 11        """Обзор продаж"""
 12        today = timezone.now().date()
 13        this_month = timezone.now().replace(day=1)
 14
 15        # Продажи за сегодня
 16        today_sales = Order.objects.filter(
 17            created_at__date=today,
 18            status='completed'
 19        ).aggregate(
 20            orders=Count('id'),
 21            revenue=Sum('total_amount'),
 22            avg_order=Avg('total_amount')
 23        )
 24
 25        # Продажи за месяц
 26        month_sales = Order.objects.filter(
 27            created_at__gte=this_month,
 28            status='completed'
 29        ).aggregate(
 30            orders=Count('id'),
 31            revenue=Sum('total_amount'),
 32            avg_order=Avg('total_amount')
 33        )
 34
 35        # Тренд продаж по дням
 36        daily_trend = Order.objects.filter(
 37            created_at__gte=this_month,
 38            status='completed'
 39        ).annotate(
 40            date=TruncDate('created_at')
 41        ).values('date').annotate(
 42            orders=Count('id'),
 43            revenue=Sum('total_amount')
 44        ).order_by('date')
 45
 46        return Response({
 47            'today': today_sales,
 48            'month': month_sales,
 49            'daily_trend': list(daily_trend)
 50        })
 51
 52    @action(detail=False, methods=['get'])
 53    def product_performance(self, request):
 54        """Производительность продуктов"""
 55        # Топ продуктов по продажам
 56        top_products = Product.objects.annotate(
 57            total_sales=Sum('order_items__quantity'),
 58            revenue=Sum(F('order_items__quantity') * F('price')),
 59            review_count=Count('reviews'),
 60            avg_rating=Avg('reviews__rating')
 61        ).filter(
 62            total_sales__gt=0
 63        ).order_by('-revenue')[:10]
 64
 65        # Категории по доходности
 66        category_performance = Category.objects.annotate(
 67            product_count=Count('products'),
 68            total_sales=Sum('products__order_items__quantity'),
 69            revenue=Sum(F('products__order_items__quantity') * F('products__price')),
 70            avg_price=Avg('products__price')
 71        ).filter(
 72            revenue__gt=0
 73        ).order_by('-revenue')
 74
 75        return Response({
 76            'top_products': ProductSerializer(top_products, many=True).data,
 77            'category_performance': CategorySerializer(category_performance, many=True).data
 78        })
 79
 80    @action(detail=False, methods=['get'])
 81    def customer_insights(self, request):
 82        """Инсайты о клиентах"""
 83        # Сегментация клиентов
 84        customer_segments = User.objects.annotate(
 85            order_count=Count('orders'),
 86            total_spent=Sum('orders__total_amount'),
 87            avg_order_value=Avg('orders__total_amount'),
 88            last_order=Max('orders__created_at')
 89        ).filter(
 90            order_count__gt=0
 91        ).aggregate(
 92            total_customers=Count('id'),
 93            avg_lifetime_value=Avg('total_spent'),
 94            high_value_customers=Count('id', filter=Q(total_spent__gte=1000)),
 95            repeat_customers=Count('id', filter=Q(order_count__gte=2))
 96        )
 97
 98        # Новые клиенты за месяц
 99        new_customers = User.objects.filter(
100            date_joined__gte=timezone.now().replace(day=1)
101        ).count()
102
103        return Response({
104            'segments': customer_segments,
105            'new_customers_month': new_customers
106        })
107
108# Создание кешированных отчетов
109class ReportService:
110    """Сервис для создания и кеширования отчетов"""
111
112    @staticmethod
113    def get_monthly_report(month, year):
114        """Месячный отчет с кешированием"""
115        cache_key = f'monthly_report_{year}_{month}'
116        report = cache.get(cache_key)
117
118        if report is None:
119            report = Order.objects.filter(
120                created_at__year=year,
121                created_at__month=month,
122                status='completed'
123            ).aggregate(
124                total_orders=Count('id'),
125                total_revenue=Sum('total_amount'),
126                avg_order_value=Avg('total_amount'),
127                unique_customers=Count('user', distinct=True),
128                top_category=Subquery(
129                    OrderItem.objects.filter(
130                        order__created_at__year=year,
131                        order__created_at__month=month
132                    ).values('product__category__name').annotate(
133                        total_sold=Sum('quantity')
134                    ).order_by('-total_sold').values('product__category__name')[:1]
135                )
136            )
137
138            # Кешируем на 24 часа
139            cache.set(cache_key, report, 86400)
140
141        return report

Лучшие практики

  • Используй annotate для добавления полей к объектам - создавай вычисляемые поля
  • Используй aggregate для общих результатов - получай итоговые значения
  • Применяй фильтры до агрегации - уменьшай объем обрабатываемых данных
  • Используй select_related и prefetch_related - избегай N+1 запросов
  • Кешируй результаты агрегации - используй Redis для часто запрашиваемых данных
  • Создавай индексы для полей агрегации - ускоряй выполнение запросов
  • Используй raw SQL для сложных агрегаций - когда Django ORM недостаточно
  • Группируй связанные агрегации - выполняй несколько вычислений за один запрос
  • Мониторь производительность - используй Django Debug Toolbar для анализа
  • Тестируй агрегационные запросы - проверяй корректность результатов

FAQ

Q: Когда использовать annotate vs aggregate?
A: annotate добавляет поля к каждому объекту в queryset, aggregate возвращает общий результат для всего queryset.

Q: Как оптимизировать агрегационные запросы?
A: Используй фильтры до агрегации, создавай индексы, применяй select_related, кешируй результаты и используй raw SQL для сложных случаев.

Q: Можно ли комбинировать несколько агрегаций?
A: Да, можно использовать несколько функций агрегации в одном запросе, как в aggregate, так и в annotate.

Q: Как группировать результаты агрегации?
A: Используй values() перед annotate для группировки по определенным полям.

Q: Что делать, если агрегация работает медленно?
A: Создавай индексы для полей агрегации, используй фильтры для уменьшения объема данных, кешируй результаты и рассмотри возможность использования raw SQL.

Q: Как обрабатывать None значения в агрегации?
A: Используй Coalesce для замены None значений на значения по умолчанию.

Q: Можно ли использовать агрегацию в API?
A: Да, агрегация отлично подходит для создания API endpoints с аналитикой, отчетами и дашбордами.