Оптимизация 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 для отслеживания времени выполнения, логируй медленные запросы и настрой алерты при превышении пороговых значений.