Оптимизация Django Admin
При работе с большими объемами данных админка может работать медленно без оптимизации. Правильная настройка может ускорить загрузку страниц в 10-100 раз.
Основные проблемы производительности
- N+1 запросы - загрузка связанных объектов в циклах
- Большие списки - загрузка тысяч записей одновременно
- Медленные фильтры - отсутствие индексов в БД
- Тяжелые вычисления - сложные аннотации и агрегации
- Отсутствие кеширования - повторные запросы к БД
Оптимизация запросов с select_related и prefetch_related
1from django.contrib import admin
2from django.db.models import Count, Avg
3from .models import Book, Author, Publisher, Category
4
5@admin.register(Book)
6class BookAdmin(admin.ModelAdmin):
7 list_display = ['title', 'author', 'publisher', 'category', 'price', 'rating']
8 list_filter = ['category', 'publisher', 'published_date']
9 search_fields = ['title', 'author__name', 'publisher__name']
10
11 # Критически важно для производительности!
12 list_select_related = ['author', 'publisher', 'category']
13
14 # Для ManyToMany отношений
15 list_prefetch_related = ['tags', 'reviews']
16
17 # Ограничиваем количество записей на странице
18 list_per_page = 25
19 list_max_show_all = 1000
20
21 def get_queryset(self, request):
22 """Оптимизированный queryset с предзагрузкой связанных объектов"""
23 return super().get_queryset(request).select_related(
24 'author', 'publisher', 'category'
25 ).prefetch_related(
26 'tags', 'reviews'
27 ).annotate(
28 review_count=Count('reviews'),
29 avg_rating=Avg('reviews__rating')
30 )
31
32 def get_search_results(self, request, queryset, search_term):
33 """Оптимизированный поиск"""
34 if search_term:
35 queryset = queryset.select_related(
36 'author', 'publisher', 'category'
37 ) | queryset.filter(
38 author__name__icontains=search_term
39 )
40 return queryset, False
Использование raw_id_fields для больших ForeignKey
1@admin.register(Order)
2class OrderAdmin(admin.ModelAdmin):
3 list_display = ['id', 'customer', 'total_amount', 'status', 'created_at']
4 list_filter = ['status', 'created_at']
5
6 # Для больших ForeignKey используем raw_id_fields
7 raw_id_fields = ['customer', 'shipping_address', 'billing_address']
8
9 # Автодополнение для поиска
10 autocomplete_fields = ['customer']
11
12 # Группируем поля для лучшей организации
13 fieldsets = (
14 ('Основная информация', {
15 'fields': ('customer', 'status', 'total_amount')
16 }),
17 ('Адреса', {
18 'fields': ('shipping_address', 'billing_address'),
19 'classes': ('collapse',)
20 }),
21 ('Детали', {
22 'fields': ('notes', 'created_at'),
23 'classes': ('collapse',)
24 })
25 )
26
27 def get_queryset(self, request):
28 return super().get_queryset(request).select_related(
29 'customer', 'shipping_address', 'billing_address'
30 )
Оптимизация фильтров и поиска
1from django.contrib.admin import SimpleListFilter
2from django.db.models import Q
3
4class PriceRangeFilter(SimpleListFilter):
5 title = 'Диапазон цен'
6 parameter_name = 'price_range'
7
8 def lookups(self, request, model_admin):
9 return (
10 ('low', 'До 1000₽'),
11 ('medium', '1000₽ - 5000₽'),
12 ('high', 'Более 5000₽'),
13 )
14
15 def queryset(self, request, queryset):
16 if self.value() == 'low':
17 return queryset.filter(price__lt=1000)
18 elif self.value() == 'medium':
19 return queryset.filter(price__gte=1000, price__lt=5000)
20 elif self.value() == 'high':
21 return queryset.filter(price__gte=5000)
22 return queryset
23
24@admin.register(Product)
25class ProductAdmin(admin.ModelAdmin):
26 list_display = ['name', 'category', 'price', 'stock', 'is_active']
27 list_filter = ['category', 'is_active', PriceRangeFilter]
28 search_fields = ['name', 'description', 'sku']
29
30 # Используем autocomplete для быстрого поиска
31 autocomplete_fields = ['category', 'supplier']
32
33 # Группируем поля для лучшей производительности
34 fieldsets = (
35 ('Основная информация', {
36 'fields': ('name', 'description', 'sku', 'category')
37 }),
38 ('Цена и наличие', {
39 'fields': ('price', 'stock', 'is_active')
40 }),
41 ('Поставщик', {
42 'fields': ('supplier', 'supplier_code'),
43 'classes': ('collapse',)
44 })
45 )
46
47 def get_search_results(self, request, queryset, search_term):
48 """Оптимизированный поиск с индексами"""
49 if search_term:
50 # Используем Q объекты для сложных запросов
51 queryset = queryset.filter(
52 Q(name__icontains=search_term) |
53 Q(description__icontains=search_term) |
54 Q(sku__icontains=search_term)
55 ).select_related('category', 'supplier')
56 return queryset, False
Кеширование и индексы
1from django.core.cache import cache
2from django.db import models
3
4class Product(models.Model):
5 name = models.CharField(max_length=200, db_index=True) # Индекс для поиска
6 category = models.ForeignKey(Category, on_delete=models.CASCADE, db_index=True)
7 price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)
8 stock = models.IntegerField(default=0)
9 created_at = models.DateTimeField(auto_now_add=True, db_index=True)
10
11 class Meta:
12 indexes = [
13 models.Index(fields=['category', 'price']), # Составной индекс
14 models.Index(fields=['is_active', 'stock']),
15 models.Index(fields=['created_at', 'category']),
16 ]
17
18@admin.register(Product)
19class ProductAdmin(admin.ModelAdmin):
20 list_display = ['name', 'category', 'price', 'stock', 'cached_rating']
21 list_per_page = 50
22
23 def cached_rating(self, obj):
24 """Кешированное поле для рейтинга"""
25 cache_key = f'product_rating_{obj.id}'
26 rating = cache.get(cache_key)
27
28 if rating is None:
29 # Вычисляем рейтинг и кешируем на 1 час
30 rating = obj.reviews.aggregate(
31 avg_rating=models.Avg('rating')
32 )['avg_rating'] or 0
33 cache.set(cache_key, rating, 3600)
34
35 return f"{rating:.1f}⭐"
36
37 cached_rating.short_description = 'Рейтинг'
38
39 def get_queryset(self, request):
40 """Кешированный queryset для часто используемых данных"""
41 cache_key = f'admin_products_{request.user.id}'
42 queryset = cache.get(cache_key)
43
44 if queryset is None:
45 queryset = super().get_queryset(request).select_related(
46 'category'
47 ).prefetch_related('reviews')
48 cache.set(cache_key, queryset, 300) # 5 минут
49
50 return queryset
Оптимизация действий (actions)
1from django.contrib import admin, messages
2from django.db import transaction
3from django.utils.html import format_html
4
5@admin.register(Product)
6class ProductAdmin(admin.ModelAdmin):
7 actions = ['bulk_update_prices', 'export_to_csv', 'deactivate_products']
8
9 def bulk_update_prices(self, request, queryset):
10 """Массовое обновление цен"""
11 updated = 0
12 try:
13 with transaction.atomic():
14 for product in queryset:
15 # Увеличиваем цену на 10%
16 product.price *= 1.1
17 product.save(update_fields=['price'])
18 updated += 1
19
20 messages.success(
21 request,
22 f'Обновлено цен для {updated} товаров'
23 )
24 except Exception as e:
25 messages.error(request, f'Ошибка обновления: {e}')
26
27 bulk_update_prices.short_description = 'Увеличить цены на 10%'
28
29 def export_to_csv(self, request, queryset):
30 """Экспорт в CSV с оптимизацией памяти"""
31 import csv
32 from django.http import HttpResponse
33
34 response = HttpResponse(content_type='text/csv')
35 response['Content-Disposition'] = 'attachment; filename="products.csv"'
36
37 writer = csv.writer(response)
38 writer.writerow(['ID', 'Название', 'Категория', 'Цена', 'Остаток'])
39
40 # Используем iterator() для больших queryset
41 for product in queryset.iterator():
42 writer.writerow([
43 product.id,
44 product.name,
45 product.category.name if product.category else '',
46 product.price,
47 product.stock
48 ])
49
50 return response
51
52 export_to_csv.short_description = 'Экспорт в CSV'
53
54 def deactivate_products(self, request, queryset):
55 """Массовая деактивация товаров"""
56 count = queryset.update(is_active=False)
57 messages.success(request, f'Деактивировано {count} товаров')
58
59 deactivate_products.short_description = 'Деактивировать товары'
Настройка пагинации и лимитов
1@admin.register(LargeDataset)
2class LargeDatasetAdmin(admin.ModelAdmin):
3 # Ограничиваем количество записей на странице
4 list_per_page = 20
5 list_max_show_all = 500
6
7 # Отключаем показ всех записей для больших таблиц
8 show_full_result_count = False
9
10 # Настраиваем пагинацию
11 paginator = admin.utils.Paginator
12
13 def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
14 """Кастомный пагинатор для больших данных"""
15 return self.paginator(
16 queryset,
17 per_page,
18 orphans=orphans,
19 allow_empty_first_page=allow_empty_first_page
20 )
21
22 def changelist_view(self, request, extra_context=None):
23 """Оптимизированное представление списка"""
24 # Добавляем контекст для мониторинга производительности
25 extra_context = extra_context or {}
26 extra_context['total_count'] = self.get_queryset(request).count()
27
28 response = super().changelist_view(request, extra_context)
29
30 # Логируем время выполнения для мониторинга
31 if hasattr(response, 'context_data'):
32 response.context_data['execution_time'] = getattr(
33 request, '_admin_execution_time', 0
34 )
35
36 return response
Мониторинг производительности
1import time
2from django.contrib import admin
3from django.db import connection
4from django.utils.deprecation import MiddlewareMixin
5
6class AdminPerformanceMiddleware(MiddlewareMixin):
7 """Middleware для мониторинга производительности админки"""
8
9 def process_request(self, request):
10 if request.path.startswith('/admin/'):
11 request._admin_start_time = time.time()
12 request._admin_query_count = len(connection.queries)
13
14 def process_response(self, request, response):
15 if hasattr(request, '_admin_start_time'):
16 execution_time = time.time() - request._admin_start_time
17 query_count = len(connection.queries) - getattr(
18 request, '_admin_query_count', 0
19 )
20
21 request._admin_execution_time = execution_time
22
23 # Логируем медленные запросы
24 if execution_time > 1.0: # Более 1 секунды
25 logger.warning(
26 f'Медленный запрос админки: {request.path} - '
27 f'{execution_time:.2f}s, {query_count} запросов'
28 )
29
30 # Добавляем заголовки для мониторинга
31 response['X-Admin-Execution-Time'] = f'{execution_time:.3f}'
32 response['X-Admin-Query-Count'] = str(query_count)
33
34 return response
35
36# Добавляем в MIDDLEWARE в settings.py
37# MIDDLEWARE = [
38# # ... другие middleware
39# 'your_app.middleware.AdminPerformanceMiddleware',
40# ]
41
42# Настройки для мониторинга в settings.py
43ADMIN_PERFORMANCE_MONITORING = {
44 'slow_query_threshold': 1.0, # секунды
45 'log_slow_queries': True,
46 'track_query_count': True,
47 'enable_profiling': False, # Включать только для отладки
48}
Настройка в settings.py
1# settings.py
2
3# Настройки админки для производительности
4ADMIN_SITE_HEADER = "Моя Админка"
5ADMIN_SITE_TITLE = "Панель управления"
6ADMIN_INDEX_TITLE = "Добро пожаловать в админку"
7
8# Настройки кеширования для админки
9CACHES = {
10 'default': {
11 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
12 'LOCATION': 'redis://127.0.0.1:6379/1',
13 'OPTIONS': {
14 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
15 }
16 }
17}
18
19# Настройки сессий для админки
20SESSION_COOKIE_AGE = 3600 # 1 час
21SESSION_SAVE_EVERY_REQUEST = False
22
23# Настройки статических файлов
24STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
25
26# Настройки логирования для админки
27LOGGING = {
28 'version': 1,
29 'disable_existing_loggers': False,
30 'handlers': {
31 'admin_file': {
32 'level': 'INFO',
33 'class': 'logging.FileHandler',
34 'filename': 'logs/admin.log',
35 'formatter': 'verbose',
36 },
37 },
38 'loggers': {
39 'django.contrib.admin': {
40 'handlers': ['admin_file'],
41 'level': 'INFO',
42 'propagate': False,
43 },
44 },
45}
Лучшие практики оптимизации
- Всегда используй select_related для ForeignKey - избегай N+1 запросов
- Применяй prefetch_related для ManyToMany - оптимизируй загрузку связанных объектов
- Используй raw_id_fields для больших ForeignKey - заменяй выпадающие списки на поля ввода
- Ограничивай list_per_page - не загружай тысячи записей одновременно
- Создавай индексы в БД - ускоряй поиск и фильтрацию
- Кешируй часто используемые данные - используй Redis или Memcached
- Мониторь производительность - отслеживай время выполнения и количество запросов
- Используй bulk операции - избегай множественных save() вызовов
- Оптимизируй поиск - применяй правильные индексы и фильтры
- Группируй поля в fieldsets - улучшай UX и производительность
FAQ
Q: Когда использовать raw_id_fields?
A: Для ForeignKey полей с большим количеством объектов (более 1000), чтобы избежать загрузки всех вариантов в выпадающий список.
Q: В чем разница между select_related и prefetch_related?
A: select_related для ForeignKey (JOIN в SQL), prefetch_related для ManyToMany (отдельные запросы с оптимизацией).
Q: Как определить, что админка работает медленно?
A: Используй Django Debug Toolbar, мониторь время выполнения, количество SQL запросов и логи производительности.
Q: Можно ли кешировать админку полностью?
A: Нет, но можно кешировать отдельные queryset, вычисляемые поля и часто используемые данные.
Q: Как оптимизировать поиск в админке?
A: Создавай индексы в БД, используй правильные поля для поиска, ограничивай результаты и применяй select_related.
Q: Что делать, если админка все равно медленная?
A: Проанализируй запросы через Django Debug Toolbar, создай недостающие индексы, используй кеширование и рассмотри возможность разделения больших таблиц.
Q: Как мониторить производительность админки в продакшене?
A: Используй middleware для отслеживания времени выполнения, логируй медленные запросы и настрой алерты при превышении пороговых значений.