Тяжелое начало
Как всегда после работы, сделал базовые дела и сел доработать эндпоинт на своем проекте. Задача пустяковая: вывести список, у каждой записи подтянуть связанный CVE и источник. Думал минут 30 и я справлюсь. Написать то написал, но подозрительно долго отрабатывались записи хоть их было и немного.
Врубил django-debug-toolbar, чекнул количество SQL-запросов на один HTTP-запрос.
201 запрос на одну страницу списка. Один — чтобы вытащить сами записи, и ещё по одному на каждую запись, чтобы подтянуть связанный источник.
Где собака зарыта
def vulnerability_list(request):
vulnerabilities = Vulnerability.objects.filter(severity="critical")
return render(request, "vulns/list.html", {"vulnerabilities": vulnerabilities})
{% for v in vulnerabilities %}
{{ v.cve_id }} — источник: {{ v.source.name }}
{% endfor %}
Каждое обращение к v.source.name - отдельный поход в базу, потому что Django ORM ленивый и подгружает связанные объекты только по факту обращения. Классический N+1.
Решение
vulnerabilities = Vulnerability.objects.filter(severity="critical").select_related("source")
select_related делает JOIN и подтягивает связанный объект одним запросом вместо отдельного на каждую строку. Запросов стало 1 вместо 201.
Проблема ManyToMany
Отдельно у каждой записи есть список тегов (ManyToManyField), и на них select_related не сработает т.к. он только для ForeignKey/OneToOne. Для этого нужен prefetch_related:
vulnerabilities = (
Vulnerability.objects
.filter(severity="critical")
.select_related("source")
.prefetch_related("tags")
)
Если коротко то prefetch_related делает отдельный запрос на все теги сразу и сшивает их в Python, а не через JOIN, для M2M это эффективнее.
Что имеем
Три секунды сократились до 50 - 60 миллисекунд. Разница на 200 записях вроде минорная, но на проде с тысячами, страшно представить с десятками тысяч записей это была бы уже не разница в UX, а разница между «работает» и «упало».
Взял на заметку:
- N+1 не видно глазами в коде — только через профилировщик запросов
select_related— для ForeignKey/OneToOne (JOIN),prefetch_related- для ManyToMany стандартный способ избежать N+1- Debug toolbar полезен и не зря его советуют на курсе, хотя знаю что на проде сеньоры-помидоры чекают логи и всякие метрики через middleware
В общем сегодня не про количество строк, а больше про оптимизацию.