N+1 в Django ORM: как найти и исправить
Автор: Кирилл Сермягин
N+1 — когда для списка из N объектов ORM делает один запрос за списком и ещё по запросу на каждый объект. На странице с 50 заказами это 51 запрос вместо двух, и время ответа растёт линейно с длиной списка.
Как найти
В разработке — django-debug-toolbar: панель SQL показывает дубли запросов. В тестах — assertNumQueries, который упадёт, если запросов стало больше.
def test_orders_page_queries(client, django_assert_num_queries, make_order):
for _ in range(20):
make_order(items=3)
with django_assert_num_queries(3):
client.get("/orders/")
Как исправить
select_related делает JOIN и подходит для ForeignKey и OneToOne. prefetch_related делает отдельный запрос и подходит для обратных связей и ManyToMany. Объект Prefetch позволяет отфильтровать и упорядочить подгружаемые данные.
from django.db.models import Prefetch
orders = (
Order.objects
.select_related("customer", "warehouse")
.prefetch_related(
Prefetch("items", queryset=OrderItem.objects.select_related("product")),
)
.order_by("-created_at")[:50]
)
Как не дать вернуться
- Тест с
django_assert_num_queriesна каждую страницу со списком. nplusoneили проверка в middleware в режиме разработки.- Логирование медленных запросов в продакшене через
pg_stat_statements.