> Напиши SQL запрос для вывода суммы продаж по каждому пользователю, учитывая, что у пользователя может не быть продаж (Go)

Уровень: middle · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

Используйте LEFT JOIN между таблицами пользователей и продаж с группировкой по пользователю. Для пользователей без продаж сумма будет NULL, поэтому оберните её в COALESCE(SUM(s.amount), 0). Запрос:

SQL
SELECT u.id, u.name, COALESCE(SUM(s.amount), 0) AS total_sales
FROM users u
LEFT JOIN sales s ON u.id = s.user_id
GROUP BY u.id, u.name;

Подробное объяснение

LEFT JOIN гарантирует, что все строки из левой таблицы (users) попадут в результат, даже если для них нет совпадений в правой (sales). Без него пользователи без продаж просто пропадут из выборки.

SUM(s.amount) для таких пользователей вернёт NULL, потому что нет строк для агрегации. COALESCE заменяет NULL на 0, что удобно для дальнейшей обработки.

Группировка по u.id, u.name обязательна - если сгруппировать только по id, в некоторых СУБД (например, MySQL с ONLY_FULL_GROUP_BY) запрос упадёт, а в PostgreSQL без строгого режима поведение будет недетерминированным.

Если в таблице продаж поле суммы называется иначе - замените amount на актуальное имя. Также можно использовать COUNT(s.id) для подсчёта количества продаж, но для суммы нужен именно SUM.

На практике

В реальных проектах часто добавляют фильтр по дате или статусу продажи. Например, только оплаченные заказы за последний месяц:

SQL
WHERE s.status = 'paid' AND s.created_at >= NOW() - INTERVAL '1 month'

Но осторожно: если фильтр поставить в WHERE, LEFT JOIN превратится в INNER JOIN - пользователи без продаж исчезнут. Фильтры по правой таблице нужно переносить в условие JOIN:

SQL
LEFT JOIN sales s ON u.id = s.user_id AND s.status = 'paid'

Также стоит подумать об индексах: sales(user_id) и, желательно, покрывающий индекс sales(user_id, amount) для ускорения агрегации.

Пример кода

Полный пример с фильтром по дате и сортировкой:

SQL
SELECT
u.id,
u.name,
COALESCE(SUM(s.amount), 0) AS total_sales
FROM users u
LEFT JOIN sales s
ON u.id = s.user_id
AND s.created_at >= '2024-01-01'
GROUP BY u.id, u.name
ORDER BY total_sales DESC;

Как отвечать на собеседовании

Начните с ключевой идеи - LEFT JOIN + COALESCE. Затем объясните, почему нельзя использовать WHERE для фильтрации по таблице продаж. Если спросят про производительность - упомяните индексы и возможность предварительной агрегации в подзапросе:

SQL
SELECT u.id, u.name, COALESCE(t.total, 0)
FROM users u
LEFT JOIN (
SELECT user_id, SUM(amount) AS total
FROM sales
GROUP BY user_id
) t ON u.id = t.user_id;

Этот вариант может быть эффективнее при большом количестве продаж на пользователя, но обычно проще и понятнее первый.

Что проверяет интервьюер

  • Понимание разницы между INNER JOIN и LEFT JOIN.
  • Знание поведения агрегатных функций с NULL.
  • Умение корректно группировать и избегать ошибок с ONLY_FULL_GROUP_BY.
  • Понимание, куда ставить условия фильтрации (в WHERE vs в ON).
  • Базовые навыки оптимизации (индексы, подзапросы).

Типичные ошибки

  • Использование INNER JOIN - теряются пользователи без продаж.
  • Забытый COALESCE - в результате NULL, а не 0.
  • Фильтр по s.amount > 0 в WHERE - превращает LEFT JOIN в INNER JOIN.
  • Группировка только по u.id без u.name - ошибка в строгих режимах SQL.
  • Использование SUM(*) или SUM(u.id) - синтаксически неверно, нужен конкретный столбец.
  • Игнорирование индексов на user_id - запрос будет медленным на больших таблицах.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью