GROUP BY и HAVING на собеседовании аналитика
Почему GROUP BY и HAVING спрашивают на каждом собеседовании
GROUP BY и агрегатные функции — основа аналитического SQL. Подсчитать количество заказов по категориям, среднюю выручку по месяцам, число уникальных пользователей за период — всё это GROUP BY. HAVING добавляет фильтрацию по агрегированным значениям: показать только категории с выручкой больше миллиона, только пользователей с тремя и более заказами.
Вопросы по GROUP BY и HAVING встречаются на собеседованиях любого уровня. Для junior — базовые агрегаты и разница между WHERE и HAVING. Для middle — GROUP BY с несколькими колонками, DISTINCT внутри агрегатов, подсчёт сложных метрик.
GROUP BY и HAVING — фундамент аналитического SQL. Если вы путаете WHERE и HAVING или не знаете порядок выполнения запроса, интервьюер это заметит в первые пять минут.
WHERE vs HAVING: ключевая разница
Это самый частый теоретический вопрос по теме. Ответ кроется в порядке выполнения SQL-запроса:
FROM — определяется источник данных. WHERE — фильтруются отдельные строки до группировки. GROUP BY — строки объединяются в группы. HAVING — фильтруются уже сформированные группы. SELECT — вычисляются выражения и агрегаты для вывода.
Из этого порядка следует главное правило: WHERE работает со строками до группировки и не может содержать агрегатные функции. HAVING работает с группами после GROUP BY и может содержать агрегатные функции.
Типичная ловушка на собеседовании: интервьюер просит отфильтровать группы по условию и смотрит, напишете ли вы WHERE или HAVING. Например, «покажите категории, в которых больше 10 товаров» — это HAVING COUNT(*) > 10, не WHERE.
Агрегатные функции: что нужно знать
COUNT(*) — считает все строки в группе, включая NULL. COUNT(column) — считает только строки, где column не NULL. Разница между ними — классический вопрос на собеседовании.
COUNT(DISTINCT column) — считает уникальные значения. Незаменим для подсчёта уникальных пользователей, уникальных заказов, уникальных дней активности. На собеседовании часто просят посчитать «число уникальных покупателей в каждой категории» — без DISTINCT результат будет неправильным.
SUM, AVG, MIN, MAX — стандартные агрегаты. Важный нюанс: все они игнорируют NULL. Если в колонке 10 значений, из которых 3 — NULL, AVG посчитает среднее по 7 значениям, а не по 10. Это может существенно исказить результат, и интервьюеры это знают.
Типичные задачи с разбором
Задача 1: Категории с выручкой выше среднего. Подход — сначала посчитать среднюю выручку по всем категориям (подзапрос), затем в HAVING отфильтровать группы, где SUM(amount) больше этого среднего. Задача проверяет умение комбинировать GROUP BY с подзапросами.
Задача 2: Пользователи, совершившие заказы в каждом месяце квартала. Подход — GROUP BY user_id и HAVING COUNT(DISTINCT month) = 3. Ловушка — использовать COUNT(month) вместо COUNT(DISTINCT month): если у пользователя несколько заказов в одном месяце, COUNT без DISTINCT покажет неправильный результат.
Задача 3: Топ-5 товаров по количеству покупок. Подход — GROUP BY product_id, ORDER BY COUNT(*) DESC, LIMIT 5. Дополнительный вопрос — что делать, если на пятом и шестом месте одинаковое количество покупок. Это отсылка к оконным функциям RANK и DENSE_RANK.
Задача 4: Средний чек по дням недели. Подход — GROUP BY по функции извлечения дня недели из даты (EXTRACT, DATE_PART или специфичная для СУБД функция). Интервьюер проверяет, знаете ли вы функции работы с датами.
GROUP BY с несколькими колонками и DISTINCT
На middle-собеседованиях часто дают задачи с группировкой по двум и более колонкам. GROUP BY category, month создаёт группу для каждой уникальной комбинации. Важно: в SELECT можно указывать только колонки из GROUP BY и агрегатные функции. Попытка вывести колонку, которой нет в GROUP BY — ошибка в PostgreSQL.
COUNT(DISTINCT user_id) — стандартный приём для подсчёта уникальных значений. DISTINCT можно использовать и внутри SUM, но на практике SUM(DISTINCT) опасен: если два заказа имеют одинаковую сумму, один будет проигнорирован.
Запомните: COUNT(DISTINCT x) — ваш главный инструмент для подсчёта уникальных значений. Используйте его каждый раз, когда в задаче звучит слово «уникальный».
Типичные ошибки кандидатов
- Используют агрегат в WHERE — пишут
WHERE COUNT(*) > 5вместоHAVING COUNT(*) > 5. SQL не позволяет агрегаты в WHERE, потому что строки ещё не сгруппированы. - Забывают DISTINCT в COUNT — считают COUNT(user_id) вместо COUNT(DISTINCT user_id), получая общее число строк вместо числа уникальных пользователей.
- Не учитывают NULL в агрегатах — AVG по колонке с NULL-значениями посчитает среднее только по непустым строкам. Если нужно учитывать NULL как ноль, используйте COALESCE.
- Добавляют в SELECT колонку без GROUP BY — например, GROUP BY category, но в SELECT ещё и product_name. В строгих СУБД это ошибка, в нестрогих — случайное значение.
Как готовиться
Порядок выполнения SQL-запроса — ключ ко всей теме. Выучите его наизусть: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. После этого вопросы про WHERE vs HAVING перестанут вызывать затруднения. Решайте задачи на подсчёт метрик — это именно то, что аналитик делает каждый день.
Помимо GROUP BY, на SQL-собеседованиях обязательно спрашивают JOIN и оконные функции. Полный обзор тем — в разделе вопросы по SQL.
FAQ
Можно ли использовать HAVING без GROUP BY?
Формально да, но это редкий случай. Без GROUP BY весь набор данных считается одной группой. На практике такое почти не встречается.
Чем COUNT(*) отличается от COUNT(column)?
COUNT() считает все строки, включая NULL. COUNT(column) считает только строки, где column не NULL. Если в таблице 100 строк и в 10 колонка email пуста, COUNT() = 100, COUNT(email) = 90.
Как посчитать несколько агрегатов по разным условиям в одном запросе?
Используйте условные агрегаты: COUNT(CASE WHEN status = 'paid' THEN 1 END) для оплаченных, SUM(CASE WHEN category = 'A' THEN amount END) для суммы по категории. Это позволяет получить несколько метрик без подзапросов.