Your Company

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.