Условие
Страница со списком ста заказов показывает имя клиента и число позиций в каждом заказе. В логах — 201 запрос к базе. Объясните, откуда они берутся, и напишите один запрос, который вернёт всё нужное.
Что требуется
- Один запрос вместо 201
- Заказы без позиций не должны пропасть
- Объяснить, как это же чинится на стороне ORM
Пример
order_id customer_name items_count 1 Иван 3 2 Пётр 0 <- заказ без позиций
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Нужны ли сами позиции или только их количество?
- Есть ли пагинация — сто заказов на странице или все сразу?
- Какая ORM: от этого зависит рецепт (select_related, prefetch_related, joinedload)
Показать решение Скрыть решение
Решение
SELECT
o.id AS order_id,
c.name AS customer_name,
COUNT(i.id) AS items_count
FROM orders o
JOIN customers c ON c.id = o.customer_id
LEFT JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, c.name
ORDER BY o.id
LIMIT 100;
-- То же самое в Django ORM:
-- (Order.objects
-- .select_related('customer')
-- .annotate(items_count=Count('items'))[:100])
Почему так
Откуда берётся 201 запрос
- Один запрос достаёт сто заказов, дальше в цикле по каждому дёргается клиент (100) и количество позиций (100)
- В ORM это выглядит как обычное обращение к атрибуту, поэтому проблему не видно в коде — её видно в логе запросов
- Время растёт линейно по числу строк на странице, и на проде это заметно сразу, а на тестовых данных — нет
Почему COUNT(i.id), а не COUNT(*)
- После LEFT JOIN у заказа без позиций есть одна строка с NULL в колонках позиций
- COUNT(*) посчитал бы её и выдал 1 вместо 0
- COUNT по конкретной колонке пропускает NULL — ровно то поведение, которое нужно
Почему select_related и prefetch_related — разные вещи
- select_related делает JOIN и подходит для «многие к одному»: клиент у заказа один
- prefetch_related делает второй запрос и склеивает в Python — это для «одного ко многим», где JOIN размножил бы строки
- Умение объяснить, почему нельзя всё решить одним JOIN, — ровно то, что проверяют этим вопросом
Что спросят дальше
- Спросят про две агрегации сразу (количество позиций и сумма): JOIN по двум таблицам умножит строки — нужны подзапросы или FILTER
- Спросят, как ловить N+1 в проде: django-debug-toolbar, логирование числа запросов на запрос, тесты с assertNumQueries
- Спросят про LIMIT с JOIN: ограничение применяется после соединения, поэтому пагинацию делают по подзапросу с заказами
Тема пройдена
Это была последняя задача темы «SQL». Возьмите следующую тему или вернитесь к разобранным через неделю — на собеседовании важно вспомнить, а не узнать.