Агрегация в 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 с аналитикой, отчетами и дашбордами.