Оптимизация Django ORM для производительности

Продвинутые методы оптимизации запросов для высоконагруженных приложений. Правильная оптимизация ORM может ускорить твое приложение в разы.

Анализ производительности запросов

Сначала научись анализировать запросы:

 1from django.db import connection
 2from django.test.utils import CaptureQueriesContext
 3
 4# Подсчет количества запросов
 5with CaptureQueriesContext(connection) as context:
 6    books = Book.objects.all()
 7    for book in books:
 8        print(book.author.name)  # N+1 проблема!
 9
10print(f"Выполнено запросов: {len(context.captured_queries)}")
11
12# Анализ медленных запросов
13from django.db import connection
14for query in connection.queries:
15    print(f"Время: {query['time']} сек")
16    print(f"SQL: {query['sql']}")

Оптимизация загрузки полей

only() - загружаем только нужные поля:

1# Загружаем только нужные поля
2books = Book.objects.only('title', 'author__name', 'price')
3
4# Вместо полной модели загружаем только необходимые данные
5book_titles = Book.objects.only('title').values_list('title', flat=True)
6
7# Для списков используй values() для словарей
8book_data = Book.objects.values('id', 'title', 'author__name')

defer() - откладываем загрузку больших полей:

1# Откладываем загрузку больших полей
2books = Book.objects.defer('description', 'content', 'full_text')
3
4# Можно комбинировать only() и defer()
5books = Book.objects.only('title', 'author').defer('author__bio')
6
7# Проверяем, какие поля загружены
8print(books.query.get_loaded_field_names())

Оптимизация связанных объектов

select_related() - для ForeignKey (JOIN):

1# Без select_related - N+1 проблема
2books = Book.objects.all()
3for book in books:
4    print(book.author.name)  # Отдельный запрос для каждой книги
5
6# С select_related - один запрос с JOIN
7books = Book.objects.select_related('author', 'publisher')
8for book in books:
9    print(book.author.name)  # Данные уже загружены

prefetch_related() - для ManyToMany и reverse ForeignKey:

 1# Без prefetch_related
 2authors = Author.objects.all()
 3for author in authors:
 4    print(f"{author.name}: {author.books.count()} книг")  # N+1 проблема
 5
 6# С prefetch_related
 7authors = Author.objects.prefetch_related('books')
 8for author in authors:
 9    print(f"{author.name}: {len(author.books.all())} книг")  # Данные предзагружены
10
11# Для сложных связей
12authors = Author.objects.prefetch_related(
13    'books',
14    'books__publisher',
15    'books__reviews'
16)

Bulk операции

bulk_create() - массовое создание:

 1# Создаем много объектов за один запрос
 2books = [
 3    Book(title=f'Book {i}', author=author, price=10.99 + i)
 4    for i in range(1000)
 5]
 6
 7# batch_size для больших объемов
 8Book.objects.bulk_create(books, batch_size=100)
 9
10# С ignore_conflicts=True для избежания дубликатов
11Book.objects.bulk_create(books, ignore_conflicts=True)

bulk_update() - массовое обновление:

 1# Обновляем много объектов за один запрос
 2books = Book.objects.filter(category='fiction')
 3for book in books:
 4    book.price = book.price * 1.1
 5    book.updated_at = timezone.now()
 6
 7# Обновляем только указанные поля
 8Book.objects.bulk_update(books, ['price', 'updated_at'], batch_size=100)
 9
10# С exclude_fields для исключения полей
11Book.objects.bulk_update(books, ['price'], exclude_fields=['updated_at'])

Оптимизация агрегаций

Используй агрегации вместо Python-логики:

 1from django.db.models import Count, Avg, Sum, Max, Min
 2
 3# Вместо Python-цикла используй агрегацию
 4# Плохо:
 5total_price = sum(book.price for book in Book.objects.all())
 6
 7# Хорошо:
 8total_price = Book.objects.aggregate(Sum('price'))['price__sum']
 9
10# Сложные агрегации
11stats = Book.objects.aggregate(
12    total_books=Count('id'),
13    avg_price=Avg('price'),
14    max_price=Max('price'),
15    min_price=Min('price')
16)
17
18# Группировка по полям
19books_by_category = Book.objects.values('category').annotate(
20    count=Count('id'),
21    avg_price=Avg('price')
22).order_by('category')

Оптимизация фильтрации

Используй эффективные фильтры:

 1# Избегай отрицательных фильтров
 2# Плохо:
 3books = Book.objects.exclude(category='fiction')
 4
 5# Лучше:
 6categories = ['non-fiction', 'sci-fi', 'mystery']
 7books = Book.objects.filter(category__in=categories)
 8
 9# Используй __in вместо множественных OR
10# Плохо:
11books = Book.objects.filter(
12    Q(category='fiction') | Q(category='sci-fi') | Q(category='mystery')
13)
14
15# Лучше:
16books = Book.objects.filter(category__in=['fiction', 'sci-fi', 'mystery'])
17
18# Избегай __contains для больших текстов
19# Плохо:
20books = Book.objects.filter(description__contains='python')
21
22# Лучше:
23books = Book.objects.filter(description__icontains='python')

Raw SQL для сложных запросов

Когда ORM не справляется:

 1from django.db import connection
 2
 3# Сложный запрос с JOIN и подзапросами
 4with connection.cursor() as cursor:
 5    cursor.execute("""
 6        SELECT
 7            a.name as author_name,
 8            COUNT(b.id) as book_count,
 9            AVG(b.price) as avg_price
10        FROM myapp_author a
11        LEFT JOIN myapp_book b ON a.id = b.author_id
12        WHERE b.published_date >= %s
13        GROUP BY a.id, a.name
14        HAVING COUNT(b.id) > 5
15        ORDER BY avg_price DESC
16    """, [date(2020, 1, 1)])
17
18    results = cursor.fetchall()
19
20# Raw SQL с параметрами
21books = Book.objects.raw("""
22    SELECT * FROM myapp_book
23    WHERE price BETWEEN %s AND %s
24    AND category = %s
25""", [10, 50, 'fiction'])

Индексы для производительности

Создавай правильные индексы:

 1# models.py
 2class Book(models.Model):
 3    title = models.CharField(max_length=200, db_index=True)
 4    author = models.ForeignKey(Author, on_delete=models.CASCADE)
 5    category = models.CharField(max_length=100)
 6    price = models.DecimalField(max_digits=10, decimal_places=2)
 7    published_date = models.DateField()
 8
 9    class Meta:
10        # Составной индекс для частых запросов
11        indexes = [
12            models.Index(fields=['category', 'published_date']),
13            models.Index(fields=['author', 'category']),
14            # Частичный индекс для дорогих книг
15            models.Index(
16                fields=['price'],
17                condition=models.Q(price__gte=100)
18            ),
19        ]

Кэширование запросов

Используй кэширование для часто запрашиваемых данных:

 1from django.core.cache import cache
 2from django.views.decorators.cache import cache_page
 3
 4# Кэширование view
 5@cache_page(60 * 15)  # 15 минут
 6def book_list(request):
 7    books = Book.objects.select_related('author').all()
 8    return render(request, 'books/list.html', {'books': books})
 9
10# Кэширование в коде
11def get_expensive_data():
12    cache_key = 'expensive_book_stats'
13    result = cache.get(cache_key)
14
15    if result is None:
16        result = Book.objects.aggregate(
17            total=Count('id'),
18            avg_price=Avg('price')
19        )
20        cache.set(cache_key, result, 300)  # 5 минут
21
22    return result
23
24# Кэширование QuerySet
25def get_popular_books():
26    cache_key = 'popular_books'
27    books = cache.get(cache_key)
28
29    if books is None:
30        books = list(Book.objects.filter(
31            rating__gte=4.5
32        ).select_related('author')[:10])
33        cache.set(cache_key, books, 600)  # 10 минут
34
35    return books

Оптимизация пагинации

Эффективная пагинация для больших списков:

 1from django.core.paginator import Paginator
 2from django.db.models import Count
 3
 4# Оптимизированная пагинация
 5def optimized_book_list(request):
 6    page = request.GET.get('page', 1)
 7
 8    # Используем count() для подсчета общего количества
 9    total_books = Book.objects.count()
10
11    # Загружаем только нужную страницу
12    books = Book.objects.select_related('author').order_by('title')[
13        (int(page) - 1) * 20:int(page) * 20
14    ]
15
16    paginator = Paginator(books, 20)
17    page_obj = paginator.get_page(page)
18
19    return render(request, 'books/list.html', {
20        'page_obj': page_obj,
21        'total_count': total_books
22    })
23
24# Cursor-based пагинация для больших данных
25def cursor_pagination(request):
26    cursor = request.GET.get('cursor')
27    limit = 20
28
29    if cursor:
30        books = Book.objects.filter(id__gt=cursor)[:limit]
31    else:
32        books = Book.objects.all()[:limit]
33
34    next_cursor = books[-1].id if books else None
35
36    return render(request, 'books/list.html', {
37        'books': books,
38        'next_cursor': next_cursor
39    })

Мониторинг производительности

Отслеживай производительность в продакшене:

 1# Middleware для логирования медленных запросов
 2import time
 3import logging
 4from django.utils.deprecation import MiddlewareMixin
 5
 6logger = logging.getLogger('django.db')
 7
 8class QueryLoggingMiddleware(MiddlewareMixin):
 9    def process_request(self, request):
10        request.start_time = time.time()
11
12    def process_response(self, request, response):
13        if hasattr(request, 'start_time'):
14            duration = time.time() - request.start_time
15            if duration > 1.0:  # Логируем запросы дольше 1 секунды
16                logger.warning(
17                    f'Медленный запрос: {request.path} - {duration:.2f}с'
18                )
19        return response
20
21# Настройка в settings.py
22LOGGING = {
23    'version': 1,
24    'disable_existing_loggers': False,
25    'handlers': {
26        'file': {
27            'level': 'WARNING',
28            'class': 'logging.FileHandler',
29            'filename': 'slow_queries.log',
30        },
31    },
32    'loggers': {
33        'django.db': {
34            'handlers': ['file'],
35            'level': 'WARNING',
36            'propagate': False,
37        },
38    },
39}

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

  • Всегда используй select_related() для ForeignKey
  • Используй prefetch_related() для ManyToMany
  • Применяй only() и defer() для больших моделей
  • Используй bulk операции для массовых изменений
  • Создавай правильные индексы для частых запросов
  • Кэшируй часто запрашиваемые данные
  • Избегай N+1 проблем
  • Используй агрегации вместо Python-логики
  • Мониторь производительность в продакшене
  • Тестируй оптимизации на реальных данных

FAQ

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

Q: Как избежать N+1 проблемы?
A: Используй select_related() для ForeignKey и prefetch_related() для ManyToMany.

Q: Когда использовать bulk операции?
A: Для создания/обновления более 100 объектов одновременно.

Q: Как оптимизировать пагинацию?
A: Используй count() для общего количества и загружай только нужную страницу.

Q: Нужно ли кэшировать все запросы?
A: Нет, только те, которые выполняются часто и редко изменяются.

Q: Как определить медленные запросы?
A: Используй django-debug-toolbar в разработке и логирование в продакшене.