select_related, prefetch_related, оптимизация запросов, кэширование
Производительность API критична для пользовательского опыта. DRF и Django предоставляют инструменты для оптимизации запросов, кэширования и ускорения ответов.
N+1 проблема — антипаттерн, когда для получения N связанных объектов выполняется N+1 запрос к БД.
# ПЛОХО: N+1 запросов
class ArticleSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'author_name']
class ArticleList(generics.ListAPIView):
queryset = Article.objects.all() # 1 запрос + N запросов к author
serializer_class = ArticleSerializerРешение: select_related для ForeignKey и OneToOne:
# ХОРОШО: 2 запроса
class ArticleList(generics.ListAPIView):
queryset = Article.objects.select_related('author').all()
serializer_class = ArticleSerializerОптимизирует ForeignKey и OneToOne связи через SQL JOIN:
# Один запрос вместо N+1
queryset = Article.objects.select_related('author').all()
# Несколько связанных полей
queryset = Article.objects.select_related('author', 'category').all()
# Вложенные связи
queryset = Article.objects.select_related(
'author__profile',
'category__parent'
).all()Когда использовать:
Оптимизирует ManyToMany и reverse ForeignKey через отдельный запрос:
# ПЛОХО: N+1 для many-to-many
class ArticleSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'tags']
class ArticleList(generics.ListAPIView):
queryset = Article.objects.all() # 1 + N запросов к tags
serializer_class = ArticleSerializer
# ХОРОШО: 2 запроса
class ArticleList(generics.ListAPIView):
queryset = Article.objects.prefetch_related('tags').all()
serializer_class = ArticleSerializerКогда использовать:
# Reverse ForeignKey
queryset = Author.objects.prefetch_related('articles').all()
# Несколько связей
queryset = Article.objects.prefetch_related(
'tags',
'comments',
'comments__author' # Вложенный prefetch
).all()| Метод | Тип связи | Механизм | Когда использовать |
|---|---|---|---|
select_related | ForeignKey, OneToOne | SQL JOIN | Связь «к одному» |
prefetch_related | ManyToMany, reverse FK | Отдельный запрос + JOIN в Python | Связь «ко многим» |
# Комбинирование обоих методов
queryset = Article.objects.select_related(
'author', # FK → JOIN
'category' # FK → JOIN
).prefetch_related(
'tags', # M2M → отдельный запрос
'comments' # reverse FK → отдельный запрос
).all()Загрузка только нужных полей:
# Загрузить только указанные поля
queryset = Article.objects.only('id', 'title', 'created_at')
# Отложить загрузку больших полей
queryset = Article.objects.defer('content', 'summary')
# Комбинирование
queryset = Article.objects.select_related('author').only(
'id', 'title', 'author_id'
)Внимание: При обращении к отложенным полям будет выполнен дополнительный запрос.
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
queryset = Article.objects.select_related(
'author',
'category'
).prefetch_related(
'tags',
Prefetch(
'comments',
queryset=Comment.objects.select_related('author').order_by('-created_at')
)
)
# Фильтрация для не-админов
if not self.request.user.is_staff:
queryset = queryset.filter(is_published=True)
return queryset
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return contextfrom django.utils.cache import patch_response_headers
from django.views.decorators.cache import cache_page
from rest_framework.response import Response
class ArticleDetail(generics.RetrieveAPIView):
queryset = Article.objects.select_related('author').all()
serializer_class = ArticleSerializer
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
patch_response_headers(response, cache_timeout=3600) # 1 час
return response
# Или через декоратор
@api_view(['GET'])
@cache_page(60 * 15) # 15 минут
def cached_article_list(request):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)from django.core.cache import cache
class ArticleSerializer(serializers.ModelSerializer):
author_name = serializers.SerializerMethodField()
def get_author_name(self, obj):
# Кэширование имени автора
cache_key = f'author_name_{obj.author_id}'
name = cache.get(cache_key)
if name is None:
name = obj.author.username
cache.set(cache_key, name, 3600)
return name
class Meta:
model = Article
fields = ['id', 'title', 'author_name']from django.core.cache import cache
def get_cached_articles():
cache_key = 'articles_list'
articles = cache.get(cache_key)
if articles is None:
articles = list(Article.objects.select_related('author').all())
cache.set(cache_key, articles, 300) # 5 минут
return articlesВсегда используйте пагинацию для больших наборов данных:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 20,
}
# CursorPagination предпочтительнее для больших данных
class ArticlePagination(CursorPagination):
ordering = '-created_at'
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100# ПЛОХО: медленно для больших списков
class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True) # Вложенный сериализатор
class Meta:
model = Article
fields = ['id', 'title', 'author']
# ХОРОШО: быстрее
class ArticleListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
author_id = serializers.IntegerField(source='author.id', read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'author_id', 'author_name']class ArticleSerializer(serializers.ModelSerializer):
# Вычисляемые поля только для чтения
comment_count = serializers.IntegerField(read_only=True)
is_recent = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'comment_count', 'is_recent']
def get_is_recent(self, obj):
return obj.created_at > timezone.now() - timedelta(days=7)Добавьте индексы для полей, по которым идёт фильтрация и сортировка:
class Article(models.Model):
title = models.CharField(max_length=200, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
slug = models.SlugField(unique=True) # Автоматический индекс
class Meta:
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['author', '-created_at']),
models.Index(fields=['category', 'is_published']),
]Django 3.1+ поддерживает async view:
from rest_framework.response import Response
from rest_framework.views import APIView
import asyncio
class AsyncArticleView(APIView):
async def get(self, request, pk):
# Асинхронный запрос к БД
article = await sync_to_async(Article.objects.get)(pk=pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)pip install django-debug-toolbarПоказывает количество SQL запросов и время выполнения.
pip install django-silkПрофилирование запросов, SQL запросы, время выполнения.
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG', # Логирует все SQL запросы
},
},
}select_related/prefetch_related — решает 90% проблем производительностиdb_index=True, Meta.indexesВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.