Вопрос с собеседования · Python

Как найти и исправить проблему N+1 запросов в Django ORM?

MiddleВеб-фреймворкиDjango
Ещё спрашивают так:
  • Чем select_related отличается от prefetch_related?
Коротко

N+1 — один запрос за списком и по запросу на каждый элемент при обращении к связанному объекту в цикле. select_related — JOIN в одном запросе для ForeignKey и OneToOne. prefetch_related — отдельный запрос на связь и склейка в Python для ManyToMany и обратных связей.

  • Найти: django-debug-toolbar, логирование SQL, assertNumQueries в тестах
  • only/defer — не тянуть лишние колонки
  • Prefetch(queryset=...) — отфильтрованная предзагрузка

А ещё Агрегаты считать в базе: annotate(Count(...)), а не len() в цикле

Подробно

Откуда берётся. Ленивые связи ORM: order.customer загружается при первом обращении. В цикле по 100 заказам — 101 запрос. В шаблонах и сериализаторах DRF это особенно незаметно.

select_related строит SQL JOIN — подходит для связей «к одному». Для «ко многим» JOIN размножил бы строки, поэтому там prefetch_related: второй запрос WHERE order_id IN (...) и раскладка результатов по объектам в памяти.

Контроль в тестах:

def test_list(self):
    with self.assertNumQueries(2):
        self.client.get("/orders/")

Тот же класс проблем есть в любом ORM: в Hibernate — fetch join и EntityGraph, в SQLAlchemy — joinedload и selectinload.

Пример

# N+1: 1 запрос за заказами + по запросу за покупателем на каждый
for order in Order.objects.all():
    print(order.customer.name)

# 1 запрос с JOIN
for order in Order.objects.select_related("customer"):
    print(order.customer.name)

# 2 запроса: заказы и все их позиции
orders = Order.objects.prefetch_related("items")

Что хотят услышать

  • Что такое N+1 и откуда.
  • select_related и prefetch_related — когда какой.
  • Как обнаружить и не допустить снова.

Проверьте себя

Один верный вариант. Подумайте, потом откройте ответ.

Показать варианты и ответ
  • ✗prefetch_related делает JOIN для ManyToMany, а select_related — отдельный запрос на каждую связь
  • ✗N+1 решается только переходом на чистый SQL
  • ✓select_related — JOIN для ForeignKey и OneToOne, prefetch_related — отдельный запрос для M2M
  • ✗N+1 возникает, только если в таблице больше миллиона строк

Разбор темы

Связанные вопросы

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования