Оптимизация 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() - откладываем загрузку больших полей:
Оптимизация связанных объектов
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 в разработке и логирование в продакшене.