Оптимизация SQL запросов и установка Debug-Toolbar

⏱ 25 мин · 🧩 заданий: 6

Проблема 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»). Там же: время рендера шаблонов, контекст, настройки, сигналы, кеш.

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")

Когда появятся теги и комментарии:

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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Список из 20 статей, у каждой в шаблоне выводится article.author.username и article.category.title. Сколько запросов без оптимизации?

Задание 2 Написать код

Оптимизируйте get_queryset в ArticleListView: подгрузите автора и категорию одним запросом (JOIN).

Задание 3 Написать код

Добавьте Debug Toolbar в маршруты только в режиме отладки: path("__debug__/", include("debug_toolbar.urls")) внутри if settings.DEBUG:.

Задание 4 Несколько вариантов

Что нужно для работы Django Debug Toolbar?

Задание 5 Написать код

Напишите тест, проверяющий, что главная страница выполняет не больше 5 запросов: используйте self.assertNumQueries(5) как контекстный менеджер вокруг self.client.get("/").

Задание 6 Один вариант ответа

Для чего prefetch_related("tags") в списке статей с тегами?

Войдите, чтобы отмечать пройденные уроки.