Задачи на подзапросы SQL на собеседовании
Карьерник — квиз-тренажёр в Telegram с 1500+ задач и вопросов для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Зачем нужны подзапросы
Подзапрос (subquery) — это SELECT внутри другого SELECT. На собеседованиях спрашивают, чтобы проверить, умеете ли вы:
- Сводить сложную логику в один запрос
- Работать с коррелированными подзапросами
- Выбирать между subquery, CTE и JOIN
- Понимать производительность
Типы подзапросов
- Скалярный — возвращает одно значение
- Столбец — возвращает список значений (с IN)
- Табличный — возвращает таблицу (в FROM)
- Коррелированный — ссылается на внешний запрос
Задачи с разборами
Задача 1. Пользователи с заказами выше среднего
Таблица orders(user_id, total).
Найти user_id, у которых сумма заказов больше среднего по всем пользователям.
Решение:
SELECT user_id, SUM(total) AS user_total
FROM orders
GROUP BY user_id
HAVING SUM(total) > (SELECT AVG(user_total) FROM (
SELECT SUM(total) AS user_total FROM orders GROUP BY user_id
) t);Идея: внутренний подзапрос считает сумму по каждому, внешний — среднее этих сумм.
Задача 2. Топ-3 товара в каждой категории
Таблица products(product_id, category, sales).
Выведите 3 товара с наибольшими продажами в каждой категории.
Решение через коррелированный подзапрос:
SELECT product_id, category, sales
FROM products p1
WHERE (
SELECT COUNT(*) FROM products p2
WHERE p2.category = p1.category AND p2.sales > p1.sales
) < 3;Лучше — через оконную функцию:
SELECT product_id, category, sales
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
) t
WHERE rn <= 3;На собесе упомяните оба — и отметьте, что оконные функции эффективнее.
Задача 3. Пользователи без заказов
users(user_id) и orders(user_id).
Решение 1 — NOT IN:
SELECT user_id FROM users
WHERE user_id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);Решение 2 — NOT EXISTS (безопаснее с NULL):
SELECT u.user_id FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);Решение 3 — LEFT JOIN:
SELECT u.user_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id
WHERE o.user_id IS NULL;На собесе покажите оба варианта. Отметьте, что NOT IN ломается на NULL в подзапросе.
Задача 4. Вторая максимальная зарплата
Таблица employees(id, salary).
Решение:
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);Через DENSE_RANK:
SELECT salary FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) t
WHERE rnk = 2;Задача 5. Процент от общей выручки
Для каждой категории — её доля в общей выручке.
Решение:
SELECT
category,
SUM(sales) AS cat_sales,
SUM(sales) * 100.0 / (SELECT SUM(sales) FROM products) AS share_percent
FROM products
GROUP BY category;Задача 6. Заказы, сделанные в первый день регистрации
users(user_id, created_at), orders(user_id, created_at, total).
Решение:
SELECT o.*
FROM orders o
WHERE o.created_at::DATE = (
SELECT u.created_at::DATE FROM users u WHERE u.user_id = o.user_id
);Или JOIN:
SELECT o.*
FROM orders o
JOIN users u ON u.user_id = o.user_id
WHERE o.created_at::DATE = u.created_at::DATE;Задача 7. Пользователи, у которых каждый заказ больше 1000
Решение через подзапрос:
SELECT user_id FROM users
WHERE user_id IN (
SELECT user_id FROM orders
GROUP BY user_id
HAVING MIN(total) > 1000
);Задача 8. Последний заказ каждого пользователя
Коррелированный подзапрос:
SELECT * FROM orders o
WHERE created_at = (
SELECT MAX(created_at) FROM orders
WHERE user_id = o.user_id
);Оконная функция (эффективнее):
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;Задача 9. Дубликаты записей
Найти строки, у которых (user_id, product_id) встречается больше 1 раза.
Решение:
SELECT * FROM orders
WHERE (user_id, product_id) IN (
SELECT user_id, product_id FROM orders
GROUP BY user_id, product_id
HAVING COUNT(*) > 1
);Задача 10. Заказы в тот же день, что последний заказ
Сравнение даты с последней датой из таблицы:
SELECT * FROM orders
WHERE created_at::DATE = (SELECT MAX(created_at)::DATE FROM orders);Задача 11. Пользователи с заказами во всех категориях
Если в системе 3 категории, найти user_id, которые купили в каждой.
Решение:
SELECT user_id FROM orders
GROUP BY user_id
HAVING COUNT(DISTINCT category) = (SELECT COUNT(DISTINCT category) FROM orders);Задача 12. Количество больше среднего по группе
Для каждой категории — товары, чьи продажи выше среднего в своей категории.
Решение:
SELECT * FROM products p1
WHERE sales > (
SELECT AVG(sales) FROM products p2 WHERE p2.category = p1.category
);Задача 13. Сравнение с предыдущим периодом
Выручка текущего месяца и предыдущего по категориям.
SELECT
category,
SUM(CASE WHEN month = DATE_TRUNC('month', CURRENT_DATE) THEN revenue END) AS cur,
SUM(CASE WHEN month = DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month') THEN revenue END) AS prev
FROM monthly_sales
GROUP BY category;Или через подзапросы:
SELECT
category,
(SELECT SUM(revenue) FROM sales WHERE category = c.category AND month = ...) AS cur
FROM categories c;Задача 14. EXISTS для условного фильтра
Пользователи, у которых был хотя бы один заказ после 2026 года.
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id
AND o.created_at > '2026-01-01'
);Задача 15. Подзапрос в SELECT (скалярный)
Вывести для каждого заказа: его сумму и среднюю сумму заказа того же пользователя.
SELECT
o.order_id,
o.total,
(SELECT AVG(total) FROM orders WHERE user_id = o.user_id) AS user_avg
FROM orders o;В лоб — неоптимально при большом количестве строк. Через окно:
SELECT
order_id,
total,
AVG(total) OVER (PARTITION BY user_id) AS user_avg
FROM orders;Подзапрос vs CTE vs JOIN
| Ситуация | Что выбрать |
|---|---|
| Одноразовое использование | Подзапрос (inline) |
| Использование результата несколько раз | CTE (WITH) |
| Соединение таблиц | JOIN |
| Фильтр «существует / не существует» | EXISTS / NOT EXISTS |
| Топ-N в группе | Оконная функция |
Частые ошибки
NOT IN и NULL
-- если в подзапросе есть NULL, вернётся пустой результат
SELECT * FROM users WHERE user_id NOT IN (SELECT user_id FROM orders);
-- безопаснее
SELECT * FROM users WHERE user_id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);Коррелированный подзапрос в SELECT — медленно
Выполняется для каждой строки. На больших таблицах — узкое место. Переписывайте через JOIN или оконную функцию.
Забытый DISTINCT в IN
x IN (SELECT ...) не требует DISTINCT, но семантически иногда хочется. Обычно работает без него.
Связанные темы
FAQ
Когда подзапрос лучше JOIN?
Когда нужно одно значение (скалярный), или фильтр «существует/не существует» (EXISTS). При соединении таблиц — JOIN эффективнее.
Когда подзапрос лучше CTE?
Для одноразового использования — подзапрос (меньше кода). Для многократного — CTE (читаемее + оптимизатор может кэшировать).
Что такое коррелированный подзапрос?
Это подзапрос, который ссылается на внешний запрос. Выполняется для каждой строки внешнего. На больших данных — медленный.
Почему NOT IN ломается на NULL?
Потому что NULL != value возвращает NULL, а не TRUE. Одна NULL в подзапросе — и весь NOT IN возвращает пусто. Используйте NOT EXISTS или LEFT JOIN.
Тренируйте SQL на реальных задачах — откройте тренажёр с 200+ задачами по SQL и 1500+ вопросами для собесов.