Оптимизация запросов Django ORM
Неэффективные запросы к базе данных - главная причина медленной работы Django приложений. Правильная оптимизация может ускорить работу в 10-100 раз.
Проблема N+1 запросов
N+1 проблема возникает когда для каждого объекта в QuerySet выполняется дополнительный запрос для получения связанных данных.
1# Неправильно - N+1 запросов
2books = Book.objects.all()
3for book in books:
4 print(f"{book.title} - {book.author.name}") # Каждый раз новый запрос!
5
6# Правильно - используем select_related
7books = Book.objects.select_related('author').all()
8for book in books:
9 print(f"{book.title} - {book.author.name}") # Автор уже загружен
Практические примеры оптимизации
1. Оптимизация ForeignKey связей (select_related)
1# Модели для примера
2class Author(models.Model):
3 name = models.CharField(max_length=100)
4 email = models.EmailField()
5 bio = models.TextField()
6
7class Book(models.Model):
8 title = models.CharField(max_length=200)
9 author = models.ForeignKey(Author, on_delete=models.CASCADE)
10 published_date = models.DateField()
11 price = models.DecimalField(max_digits=10, decimal_places=2)
12
13class Review(models.Model):
14 book = models.ForeignKey(Book, on_delete=models.CASCADE)
15 reviewer_name = models.CharField(max_length=100)
16 rating = models.IntegerField()
17 comment = models.TextField()
18
19# Неоптимизированный код
20books = Book.objects.all()
21for book in books:
22 print(f"Книга: {book.title}")
23 print(f"Автор: {book.author.name}") # N+1 запрос!
24 print(f"Email автора: {book.author.email}") # Еще один запрос!
25 print("---")
26
27# Оптимизированный код с select_related
28books = Book.objects.select_related('author').all()
29for book in books:
30 print(f"Книга: {book.title}")
31 print(f"Автор: {book.author.name}") # Данные уже загружены
32 print(f"Email автора: {book.author.email}") # Данные уже загружены
33 print("---")
34
35# Глубокие связи (несколько уровней)
36reviews = Review.objects.select_related('book__author').all()
37for review in reviews:
38 print(f"Отзыв на книгу: {review.book.title}")
39 print(f"Автор: {review.book.author.name}") # Все данные загружены одним запросом
40 print(f"Рейтинг: {review.rating}")
41 print("---")
2. Оптимизация ManyToMany связей (prefetch_related)
1# Модели с ManyToMany
2class Category(models.Model):
3 name = models.CharField(max_length=100)
4 description = models.TextField()
5
6class Book(models.Model):
7 title = models.CharField(max_length=200)
8 author = models.ForeignKey(Author, on_delete=models.CASCADE)
9 categories = models.ManyToManyField(Category)
10 published_date = models.DateField()
11
12# Неоптимизированный код
13books = Book.objects.all()
14for book in books:
15 print(f"Книга: {book.title}")
16 for category in book.categories.all(): # N+1 запрос!
17 print(f" Категория: {category.name}")
18 print("---")
19
20# Оптимизированный код с prefetch_related
21books = Book.objects.prefetch_related('categories').all()
22for book in books:
23 print(f"Книга: {book.title}")
24 for category in book.categories.all(): # Данные уже загружены
25 print(f" Категория: {category.name}")
26 print("---")
27
28# Комбинирование select_related и prefetch_related
29books = Book.objects.select_related('author').prefetch_related('categories').all()
30for book in books:
31 print(f"Книга: {book.title}")
32 print(f"Автор: {book.author.name}") # ForeignKey - select_related
33 for category in book.categories.all(): # ManyToMany - prefetch_related
34 print(f" Категория: {category.name}")
35 print("---")
3. Оптимизация с помощью only() и defer()
1# Загружаем только нужные поля
2# Полезно для больших текстовых полей или когда нужны только определенные данные
3
4# Загружаем только title и author__name
5books = Book.objects.select_related('author').only('title', 'author__name').all()
6for book in books:
7 print(f"{book.title} - {book.author.name}")
8 # book.description будет недоступно (вызовет ошибку)
9 # book.author.email будет недоступно
10
11# Откладываем загрузку больших полей
12books = Book.objects.defer('description', 'content').all()
13for book in books:
14 print(f"Название: {book.title}")
15 # book.description загрузится только при обращении к нему
16
17# Комбинирование only и defer
18books = Book.objects.only('title', 'author__name').defer('author__bio').all()
19for book in books:
20 print(f"{book.title} - {book.author.name}")
21 # author.bio будет загружено только при обращении
4. Оптимизация агрегации и аннотаций
1from django.db.models import Count, Sum, Avg, Max, Min
2
3# Неоптимизированный код
4authors = Author.objects.all()
5for author in authors:
6 book_count = author.book_set.count() # N+1 запрос!
7 total_price = sum(book.price for book in author.book_set.all()) # Еще N+1 запрос!
8 print(f"{author.name}: {book_count} книг, общая стоимость: {total_price}")
9
10# Оптимизированный код с annotate
11authors = Author.objects.annotate(
12 book_count=Count('book'),
13 total_price=Sum('book__price'),
14 avg_price=Avg('book__price'),
15 max_price=Max('book__price'),
16 min_price=Min('book__price')
17).all()
18
19for author in authors:
20 print(f"{author.name}: {author.book_count} книг")
21 print(f" Общая стоимость: {author.total_price or 0}")
22 print(f" Средняя цена: {author.avg_price or 0}")
23 print(f" Максимальная цена: {author.max_price or 0}")
24 print(f" Минимальная цена: {author.min_price or 0}")
25 print("---")
26
27# Группировка по категориям
28category_stats = Category.objects.annotate(
29 book_count=Count('book'),
30 avg_price=Avg('book__price')
31).filter(book_count__gt=0).order_by('-book_count')
32
33for category in category_stats:
34 print(f"Категория: {category.name}")
35 print(f" Книг: {category.book_count}")
36 print(f" Средняя цена: {category.avg_price or 0}")
37 print("---")
5. Оптимизация с помощью bulk операций
1# Неоптимизированное создание объектов
2for i in range(1000):
3 Book.objects.create(
4 title=f'Книга {i}',
5 author=author,
6 published_date='2024-01-01',
7 price=10.00
8 ) # 1000 отдельных INSERT запросов!
9
10# Оптимизированное создание с bulk_create
11books_to_create = []
12for i in range(1000):
13 books_to_create.append(Book(
14 title=f'Книга {i}',
15 author=author,
16 published_date='2024-01-01',
17 price=10.00
18 ))
19
20Book.objects.bulk_create(books_to_create) # Один запрос для всех книг!
21
22# Bulk обновление
23books = Book.objects.filter(price__lt=20.00)
24for book in books:
25 book.price = book.price * 1.1 # Увеличиваем цену на 10%
26
27Book.objects.bulk_update(books, ['price']) # Один UPDATE запрос!
28
29# Bulk удаление
30old_books = Book.objects.filter(published_date__year__lt=2020)
31old_books.delete() # Один DELETE запрос!
6. Оптимизация с помощью raw SQL
1# Когда ORM не справляется со сложными запросами
2from django.db import connection
3
4# Сложный запрос с JOIN и подзапросами
5with connection.cursor() as cursor:
6 cursor.execute("""
7 SELECT
8 a.name as author_name,
9 COUNT(b.id) as book_count,
10 AVG(b.price) as avg_price,
11 SUM(b.price) as total_revenue
12 FROM myapp_author a
13 LEFT JOIN myapp_book b ON a.id = b.author_id
14 WHERE b.published_date >= '2020-01-01'
15 GROUP BY a.id, a.name
16 HAVING COUNT(b.id) > 5
17 ORDER BY total_revenue DESC
18 """)
19
20 results = cursor.fetchall()
21 for row in results:
22 print(f"Автор: {row[0]}")
23 print(f" Книг: {row[1]}")
24 print(f" Средняя цена: {row[2]}")
25 print(f" Общий доход: {row[3]}")
26 print("---")
27
28# Или используй raw() для моделей
29books = Book.objects.raw("""
30 SELECT b.*, a.name as author_name
31 FROM myapp_book b
32 JOIN myapp_author a ON b.author_id = a.id
33 WHERE b.price > %s
34 ORDER BY b.published_date DESC
35""", [20.00])
36
37for book in books:
38 print(f"{book.title} - {book.author_name}")
Профилирование и мониторинг запросов
Настройка логирования SQL запросов
1# settings.py
2LOGGING = {
3 'version': 1,
4 'disable_existing_loggers': False,
5 'handlers': {
6 'console': {
7 'class': 'logging.StreamHandler',
8 },
9 },
10 'loggers': {
11 'django.db.backends': {
12 'handlers': ['console'],
13 'level': 'DEBUG',
14 },
15 },
16}
17
18# Или используй django-debug-toolbar для разработки
19INSTALLED_APPS = [
20 'debug_toolbar',
21]
22
23MIDDLEWARE = [
24 'debug_toolbar.middleware.DebugToolbarMiddleware',
25]
26
27INTERNAL_IPS = [
28 '127.0.0.1',
29]
Мониторинг производительности в коде
1import time
2from django.db import connection
3
4# Функция для измерения времени выполнения запросов
5def measure_queries(func):
6 def wrapper(*args, **kwargs):
7 initial_queries = len(connection.queries)
8 start_time = time.time()
9
10 result = func(*args, **kwargs)
11
12 end_time = time.time()
13 final_queries = len(connection.queries)
14
15 print(f"Функция {func.__name__}:")
16 print(f" Время выполнения: {end_time - start_time:.4f} секунд")
17 print(f" Количество запросов: {final_queries - initial_queries}")
18 print(f" Общее время SQL: {sum(float(q['time']) for q in connection.queries[initial_queries:final_queries]):.4f} секунд")
19
20 return result
21 return wrapper
22
23# Использование декоратора
24@measure_queries
25def get_books_with_authors():
26 return Book.objects.select_related('author').all()
27
28@measure_queries
29def get_books_without_optimization():
30 return Book.objects.all()
31
32# Сравниваем производительность
33print("С оптимизацией:")
34books1 = get_books_with_authors()
35
36print("\nБез оптимизации:")
37books2 = get_books_without_optimization()
Создание и использование индексов
Добавление индексов в модели
1class Book(models.Model):
2 title = models.CharField(max_length=200, db_index=True) # Индекс для поиска
3 author = models.ForeignKey(Author, on_delete=models.CASCADE, db_index=True)
4 published_date = models.DateField(db_index=True) # Индекс для фильтрации по дате
5 price = models.DecimalField(max_length=10, decimal_places=2, db_index=True)
6 isbn = models.CharField(max_length=13, unique=True) # Уникальный индекс
7
8 class Meta:
9 # Составной индекс для часто используемых комбинаций
10 indexes = [
11 models.Index(fields=['author', 'published_date']),
12 models.Index(fields=['price', 'published_date']),
13 # Индекс для полнотекстового поиска (PostgreSQL)
14 models.Index(fields=['title'], name='book_title_gin_idx'),
15 ]
16
17# Создание индекса через миграцию
18class Migration(migrations.Migration):
19 dependencies = [
20 ('myapp', '0001_initial'),
21 ]
22
23 operations = [
24 migrations.AddIndex(
25 model_name='book',
26 index=models.Index(
27 fields=['author', 'published_date'],
28 name='book_author_date_idx'
29 ),
30 ),
31 ]
Анализ использования индексов
1# PostgreSQL - анализ запроса
2from django.db import connection
3
4with connection.cursor() as cursor:
5 cursor.execute("""
6 EXPLAIN (ANALYZE, BUFFERS)
7 SELECT * FROM myapp_book
8 WHERE author_id = 1 AND published_date > '2020-01-01'
9 """)
10
11 plan = cursor.fetchall()
12 for row in plan:
13 print(row[0])
14
15# MySQL - анализ запроса
16with connection.cursor() as cursor:
17 cursor.execute("""
18 EXPLAIN FORMAT=JSON
19 SELECT * FROM myapp_book
20 WHERE author_id = 1 AND published_date > '2020-01-01'
21 """)
22
23 plan = cursor.fetchall()
24 print(plan)
Кэширование запросов
Настройка кэширования
1# settings.py
2CACHES = {
3 'default': {
4 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
5 'LOCATION': 'redis://127.0.0.1:6379/1',
6 }
7}
8
9# Кэширование в коде
10from django.core.cache import cache
11from django.views.decorators.cache import cache_page
12
13# Кэширование view
14@cache_page(60 * 15) # Кэш на 15 минут
15def book_list(request):
16 books = Book.objects.select_related('author').all()
17 return render(request, 'books/list.html', {'books': books})
18
19# Кэширование QuerySet
20def get_popular_books():
21 cache_key = 'popular_books'
22 books = cache.get(cache_key)
23
24 if books is None:
25 books = Book.objects.select_related('author').annotate(
26 review_count=Count('review')
27 ).order_by('-review_count')[:10]
28
29 # Кэшируем на 1 час
30 cache.set(cache_key, books, 60 * 60)
31
32 return books
33
34# Кэширование с версионированием
35def get_author_books(author_id):
36 cache_key = f'author_books_{author_id}'
37 books = cache.get(cache_key)
38
39 if books is None:
40 books = Book.objects.filter(author_id=author_id).select_related('author')
41 cache.set(cache_key, books, 60 * 30) # 30 минут
42
43 return books
44
45# Инвалидация кэша при изменении данных
46def invalidate_author_cache(author_id):
47 cache_key = f'author_books_{author_id}'
48 cache.delete(cache_key)
Лучшие практики оптимизации
- Всегда используй select_related для ForeignKey - это самый простой способ избежать N+1
- Используй prefetch_related для ManyToMany - загружай связанные объекты одним запросом
- Применяй only() и defer() для больших полей - загружай только нужные данные
- Используй bulk операции для массовых изменений - это в разы быстрее
- Создавай индексы для часто фильтруемых полей - особенно для дат и внешних ключей
- Кэшируй часто запрашиваемые данные - используй Redis или Memcached
- Профилируй запросы в development - используй django-debug-toolbar
- Мониторь производительность в production - следи за временем выполнения запросов
Частые ошибки и их решения
Ошибка: "FieldError: Cannot resolve keyword"
Ошибка: "QuerySet is not iterable"
1# Проблема: использование only() с полями, которые не загружены
2books = Book.objects.only('title').all()
3
4# Неправильно
5for book in books:
6 print(book.author.name) # Ошибка! author не загружен
7
8# Правильно
9books = Book.objects.select_related('author').only('title', 'author__name').all()
10for book in books:
11 print(book.author.name) # Работает
FAQ
Q: Когда использовать select_related?
A: Для ForeignKey и OneToOneField когда нужны связанные объекты. Это загружает данные одним JOIN запросом.
Q: Когда использовать prefetch_related?
A: Для ManyToManyField и reverse ForeignKey. Это загружает связанные объекты отдельным запросом.
Q: Как определить N+1 проблему?
A: Включи логирование SQL запросов и посмотри на количество запросов при итерации по QuerySet.
Q: Когда использовать raw SQL?
A: Для сложных запросов, которые сложно выразить через ORM, или когда нужна максимальная производительность.
Q: Как оптимизировать поиск по тексту?
A: Используй полнотекстовый поиск PostgreSQL, создавай GIN индексы для текстовых полей.
Q: Можно ли кэшировать QuerySet?
A: Да, но лучше кэшировать результаты, а не сам QuerySet. Используй cache.get() и cache.set().