Задачи на агрегацию SQL на собеседовании
Карьерник — квиз-тренажёр в Telegram с 1500+ задач и вопросов для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Что проверяют
Агрегация — 50% любого собеседования аналитика. Проверяют:
- GROUP BY с несколькими колонками
- HAVING vs WHERE
- Агрегаты с условиями (через CASE)
- COUNT DISTINCT
- Работу с NULL в агрегатах
Задачи с разборами
Задача 1. Количество пользователей по странам
users(user_id, country)
SELECT country, COUNT(*) AS users_count
FROM users
GROUP BY country
ORDER BY users_count DESC;Ловушка: COUNT(*) считает все строки, включая NULL. COUNT(country) — пропускает NULL. На собесе спросят: знаете ли разницу?
Задача 2. Средний чек в выходные и будни
orders(created_at, total)
SELECT
CASE
WHEN EXTRACT(DOW FROM created_at) IN (0, 6) THEN 'weekend'
ELSE 'weekday'
END AS day_type,
AVG(total) AS avg_check,
COUNT(*) AS orders_cnt
FROM orders
GROUP BY 1;Задача 3. Топ-5 категорий по выручке
orders(order_id, category, total)
SELECT category, SUM(total) AS revenue
FROM orders
GROUP BY category
ORDER BY revenue DESC
LIMIT 5;Задача 4. Количество уникальных товаров на заказ
order_items(order_id, product_id, qty)
SELECT
order_id,
COUNT(DISTINCT product_id) AS unique_products,
SUM(qty) AS total_items
FROM order_items
GROUP BY order_id;Задача 5. Категории, где больше 100 заказов
SELECT category, COUNT(*) AS orders_count
FROM orders
GROUP BY category
HAVING COUNT(*) > 100;Ключевое: HAVING — для фильтрации после агрегации. WHERE — до.
Задача 6. Доля заказов со скидкой
orders(order_id, has_discount BOOL)
SELECT
COUNT(*) AS total_orders,
SUM(CASE WHEN has_discount THEN 1 ELSE 0 END) AS discounted,
AVG(CASE WHEN has_discount THEN 1.0 ELSE 0 END) AS discount_share
FROM orders;Постгрес-specific: можно AVG(has_discount::int) или COUNT(*) FILTER (WHERE has_discount).
Задача 7. Средняя сумма для пользователей с 3+ заказами
SELECT
user_id,
AVG(total) AS avg_order,
COUNT(*) AS orders_cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 3;Задача 8. Максимум и минимум в одном запросе
SELECT
MAX(total) AS max_total,
MIN(total) AS min_total,
AVG(total) AS avg_total,
MAX(total) - MIN(total) AS RANGE
FROM orders;Задача 9. Выручка по месяцам
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue,
COUNT(*) AS orders,
COUNT(DISTINCT user_id) AS buyers
FROM orders
GROUP BY 1
ORDER BY 1;Задача 10. Топ-10 пользователей по числу заказов
SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
ORDER BY orders_count DESC
LIMIT 10;Задача 11. Категории без заказов в апреле
SELECT c.category
FROM categories c
LEFT JOIN orders o
ON o.category = c.category
AND o.created_at >= '2026-04-01'
AND o.created_at < '2026-05-01'
GROUP BY c.category
HAVING COUNT(o.order_id) = 0;Критично: условие даты идёт в ON, а не в WHERE, иначе LEFT JOIN превратится в INNER.
Задача 12. Средняя сумма второго заказа по когортам
Сложнее. orders + когорта по месяцу первого заказа.
WITH user_orders AS (
SELECT
user_id,
total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num,
MIN(created_at) OVER (PARTITION BY user_id) AS first_order_at
FROM orders
)
SELECT
DATE_TRUNC('month', first_order_at) AS cohort_month,
AVG(total) AS avg_second_order
FROM user_orders
WHERE order_num = 2
GROUP BY 1;Задача 13. Доля возвратов по категориям
orders(category, total, status)
SELECT
category,
COUNT(*) AS total,
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) AS refunds,
ROUND(100.0 * SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) / COUNT(*), 2) AS refund_pct
FROM orders
GROUP BY category
ORDER BY refund_pct DESC;Задача 14. Количество активных дней пользователя
SELECT
user_id,
COUNT(DISTINCT DATE(event_at)) AS active_days
FROM events
GROUP BY user_id;Задача 15. Пользователи с одинаковым количеством заказов
SELECT orders_count, COUNT(*) AS users_cnt
FROM (
SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
) t
GROUP BY orders_count
ORDER BY orders_count;Частые ошибки
WHERE vs HAVING
-- неправильно — агрегат в WHERE
SELECT category, COUNT(*) FROM orders
WHERE COUNT(*) > 100 -- ошибка
GROUP BY category;
-- правильно
SELECT category, COUNT(*) FROM orders
GROUP BY category
HAVING COUNT(*) > 100;Правило:
- WHERE — до группировки (фильтрует строки)
- HAVING — после (фильтрует группы)
COUNT(*) vs COUNT(column) vs COUNT(DISTINCT column)
COUNT(*)— все строкиCOUNT(column)— ненулевые значения columnCOUNT(DISTINCT column)— уникальные значения
Агрегат без GROUP BY
-- ошибка: user_id не агрегирован и не в GROUP BY
SELECT user_id, SUM(total) FROM orders;
-- правильно
SELECT user_id, SUM(total) FROM orders GROUP BY user_id;NULL в агрегатах
SUM, AVG, COUNT(col) игнорируют NULL. Но AVG считает от только от не-NULL записей — если хотите от всех, используйте SUM / COUNT(*).
Смешивание разных гранулярностей
-- неправильно: sum orders и sum items в одном запросе — разные размерности
SELECT SUM(total), SUM(qty) FROM orders JOIN order_items USING (order_id);Разносите по CTE или подзапросам.
Производительность
- Индекс на GROUP BY-колонку ускоряет
- COUNT(DISTINCT) — дороже, чем COUNT(*)
- На больших данных: партиционирование по времени
Связанные темы
- GROUP BY — шпаргалка
- HAVING — шпаргалка
- Агрегатные функции — шпаргалка
- Задачи на JOIN на собеседовании
FAQ
Что спрашивают чаще всего по агрегации?
COUNT vs COUNT DISTINCT, WHERE vs HAVING, SUM с CASE (доля чего-то), группировка по дате.
Как отличить WHERE от HAVING?
WHERE — до GROUP BY, фильтрует строки. HAVING — после, фильтрует агрегаты.
COUNT(*) или COUNT(1) — что быстрее?
В современных СУБД — одинаково. Пишите COUNT(*), это стандарт.
Почему SUM возвращает NULL, а не 0 для пустой группы?
Стандарт SQL. Для принудительного 0 — COALESCE(SUM(col), 0).
Тренируйте SQL на реальных задачах — откройте тренажёр с 200+ задачами и разборами.