Оптимизация SQL запросов и установка Debug-Toolbar
Проблема N+1
Шаблон списка статей обращается к связанным объектам:
{% for article in articles %}
{{ article.author.username }} · {{ article.category.title }}
{% endfor %}
Для каждой статьи Django делает отдельный запрос за автором и ещё один — за категорией. 20 статей → 1 + 20 + 20 = 41 запрос. Это и есть проблема N+1 — самая частая причина медленных Django-сайтов.
Django Debug Toolbar
Чтобы увидеть проблему, установим панель отладки:
pip install django-debug-toolbar
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware", # как можно выше
...
]
INTERNAL_IPS = ["127.0.0.1"]
# backend/urls.py
if settings.DEBUG:
urlpatterns = [path("__debug__/", include("debug_toolbar.urls"))] + urlpatterns
Справа на странице появится панель. Вкладка SQL покажет все запросы, их время и подсветит дубликаты («similar» и «duplicated»). Там же: время рендера шаблонов, контекст, настройки, сигналы, кеш.
select_related: JOIN для ForeignKey
class ArticleListView(ListView):
def get_queryset(self):
return Article.custom.all().select_related("author", "category")
Теперь один запрос с двумя JOIN. 41 → 1.
Для детальной страницы — то же самое:
class ArticleDetailView(DetailView):
queryset = Article.custom.all().select_related("author", "category")
prefetch_related: для ManyToMany и обратных связей
Когда появятся теги и комментарии:
Article.custom.all().select_related("author", "category").prefetch_related("tags")
prefetch_related делает один дополнительный запрос за тегами всех статей страницы (WHERE article_id IN (...)) и раскладывает их по статьям. Итого 2 запроса вместо 1 + N.
Тонкая настройка — объект Prefetch:
from django.db.models import Prefetch
Article.objects.prefetch_related(
Prefetch("comments", queryset=Comment.objects.filter(status="published").select_related("author"))
)
Меньше данных: only и defer
Полное описание статьи в списке не нужно:
Article.custom.all().select_related("author", "category").defer("full_description")
defer не загружает тяжёлое поле. Но если шаблон всё-таки к нему обратится — будет запрос на каждую статью. Проверяйте в Debug Toolbar.
Агрегаты вместо циклов
Плохо — счётчик комментариев отдельным запросом в каждой карточке:
{{ article.comments.count }}
Хорошо — посчитать в базе одним запросом:
Article.custom.all().annotate(comments_count=Count("comments"))
{{ article.comments_count }}
Индексы
Поля, по которым часто фильтруют и сортируют, индексируют:
class Meta:
indexes = [models.Index(fields=["-fixed", "-time_create", "status"])]
ForeignKey и SlugField индексируются автоматически.
Защита тестами
Чтобы оптимизация не «сломалась» незаметно, зафиксируйте число запросов:
class PerformanceTests(TestCase):
def test_home_queries(self):
# создать 10 статей...
with self.assertNumQueries(5):
self.client.get("/")
Если кто-то уберёт select_related, тест упадёт.
Итог
- N+1 — по запросу на каждый объект при обращении к связям в цикле.
- Debug Toolbar показывает все запросы и дубликаты.
select_related(JOIN) — для ForeignKey/OneToOne,prefetch_related— для ManyToMany и обратных связей.annotateвместо.count()в шаблоне,defer/onlyдля тяжёлых полей, индексы для частых фильтров.assertNumQueriesзащищает от регрессий.
Практика решено 0 из 6
Список из 20 статей, у каждой в шаблоне выводится article.author.username и article.category.title. Сколько запросов без оптимизации?
Оптимизируйте get_queryset в ArticleListView: подгрузите автора и категорию одним запросом (JOIN).
def get_queryset(self):
return Article.custom.all().select_related("author", "category")
Добавьте Debug Toolbar в маршруты только в режиме отладки: path("__debug__/", include("debug_toolbar.urls"))
внутри if settings.DEBUG:.
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("modules.blog.urls")),
]
if settings.DEBUG:
urlpatterns = [path("__debug__/", include("debug_toolbar.urls"))] + urlpatterns
Что нужно для работы Django Debug Toolbar?
Напишите тест, проверяющий, что главная страница выполняет не больше 5 запросов:
используйте self.assertNumQueries(5) как контекстный менеджер вокруг self.client.get("/").
class PerformanceTests(TestCase):
def test_home_queries(self):
with self.assertNumQueries(5):
self.client.get("/")
Для чего prefetch_related("tags") в списке статей с тегами?
Войдите, чтобы отмечать пройденные уроки.