SQL на собеседовании аналитика: 6 задач с решениями
SQL-задачи для собеседования аналитика данных и продуктового аналитика: JOIN, NULL, конверсия, оконные функции и повторные покупки. Решения и типичные ошибки.
На SQL-секции важно получить корректный результат и объяснить логику запроса. Даже короткая задача может проверять гранулярность данных, дубли после JOIN, знаменатель конверсии и смысл оконной функции.
Какие темы SQL подготовить аналитику
- WHERE и HAVING, порядок фильтрации и агрегации.
- INNER JOIN и LEFT JOIN, связь «один ко многим», уникальность ключей.
- COUNT, COUNT DISTINCT, SUM и NULL.
- CTE и подзапросы для разделения шагов решения.
- ROW_NUMBER, LAG и накопительные агрегаты.
- Воронки, конверсия, первые и повторные покупки.
Перед запросом уточни: что означает одна строка, какой период нужен, какой статус считать покупкой и как учитывать возвраты. В примерах ниже orders содержит ровно одну строку на заказ: order_id, user_id, paid_at, status, amount. Заказ засчитывается при status = 'paid'. users содержит одного пользователя на строку, а events — событие с полями event_id, user_id, event_time, event_name.
Задача 1. Пользователи с двумя покупками
Найди пользователей, которые сделали не меньше двух оплаченных заказов. В рамках этой задачи период не ограничен.
SELECT user_id, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
GROUP BY user_id
HAVING COUNT(*) >= 2;
Почему так: WHERE отбирает оплаченные заказы до группировки. HAVING проверяет уже посчитанное число заказов пользователя. COUNT(*) подходит, потому что по условию строка соответствует уникальному заказу.
Что меняет ответ: если таблица хранит историю статусов, сначала получи одну актуальную запись заказа. Если нужен определённый период, добавь фильтр по времени оплаты.
Задача 2. Пользователи без покупок
Выведи пользователей из users, у которых нет ни одного оплаченного заказа.
SELECT u.user_id
FROM users AS u
LEFT JOIN orders AS o
ON o.user_id = u.user_id
AND o.status = 'paid'
WHERE o.order_id IS NULL;
Фильтр оплаченных заказов стоит в ON: так LEFT JOIN сохраняет пользователей без подходящих записей. Проверяем order_id, который по условию не бывает NULL в настоящем заказе. Эквивалентное решение возможно через NOT EXISTS.
Типичная ошибка: перенести o.status = 'paid' в WHERE. Тогда строки без заказа будут отфильтрованы. Связанный разбор — COUNT и NULL в SQL.
Задача 3. Конверсия зарегистрированных в покупку
Посчитай долю пользователей из users, у которых есть хотя бы один оплаченный заказ. Это накопительная конверсия на момент выгрузки; фиксированное окно после регистрации в этой задаче не задано.
WITH buyers AS (
SELECT DISTINCT user_id
FROM orders
WHERE status = 'paid'
)
SELECT
COUNT(b.user_id) * 1.0 / NULLIF(COUNT(u.user_id), 0)
AS purchase_conversion
FROM users AS u
LEFT JOIN buyers AS b ON b.user_id = u.user_id;
Сначала получаем одного покупателя на строку. Несколько заказов одного человека не должны увеличивать число пользователей в знаменателе. Для пустой базы результат NULL, поскольку конверсия не определена.
Для сравнения когорт понадобится одинаковый срок наблюдения. Накопительная конверсия старой и новой когорты отвечает на разные по горизонту вопросы. Правила расчёта показателей подробнее разобраны в статье о продуктовых метриках.
Задача 4. Последнее событие пользователя
Найди последнее событие каждого пользователя. При одинаковом времени выбирай запись с большим event_id; event_id уникален, event_time заполнен.
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY event_time DESC, event_id DESC
) AS rn
FROM events
)
SELECT user_id, event_name, event_time
FROM ranked
WHERE rn = 1;
ROW_NUMBER даёт ровно одну запись. Дополнительный ключ делает результат определённым при совпадении времени. MAX(event_time) находит время, но сам по себе не возвращает название нужного события.
Задача 5. Вторая оплаченная покупка
Выведи вторую оплаченную покупку каждого пользователя. Для оплаченных заказов paid_at заполнен. При одинаковом времени порядок определяется по order_id.
WITH ranked AS (
SELECT order_id, user_id, paid_at,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY paid_at, order_id
) AS purchase_number
FROM orders
WHERE status = 'paid'
)
SELECT user_id, order_id, paid_at
FROM ranked
WHERE purchase_number = 2;
Пользователь с одной покупкой не попадёт в ответ. Важно ранжировать после отбора оплаченных заказов. Если нужна длительность между покупками, её можно посчитать через LAG по тому же порядку.
Задача 6. Почему JOIN удвоил выручку
В order_items может быть несколько товарных позиций одного заказа. Запрос соединяет её с orders и суммирует orders.amount. Для заказа с тремя позициями сумма заказа будет учтена трижды.
Если задача — найти выручку оплаченных заказов, содержащих хотя бы одну позицию категории «books», можно проверить наличие через EXISTS:
SELECT COALESCE(SUM(o.amount), 0) AS revenue
FROM orders AS o
WHERE o.status = 'paid'
AND EXISTS (
SELECT 1
FROM order_items AS i
WHERE i.order_id = o.order_id
AND i.category = 'books'
);
Обрати внимание: это выручка целых заказов, в которых есть книги. Для выручки только книжных позиций нужны суммы самих позиций. SUM(DISTINCT amount) не исправляет ошибку: два разных заказа могут иметь одинаковую стоимость.
Как решать задачу вслух на собеседовании
- Перескажи требуемый результат: какой объект должен быть в одной строке ответа.
- Уточни ключи, время, статусы, пропуски и повторы.
- Раздели решение на шаги: отбор → соединение → агрегация → окно.
- Проверь на маленьком примере, включая пустую выборку и несколько заказов одного человека.
- Объясни ограничения и то, что изменишь при другом условии.
Незнакомая функция не отменяет понимания задачи. Сначала объясни логику, затем уточни доступный SQL-диалект. В ClickHouse некоторые агрегаты, даты и поведение JOIN отличаются от PostgreSQL; конкретный запрос нужно проверять в выбранной СУБД.
План подготовки по SQL на неделю
Это цикл самопроверки для человека, который уже знаком с основами SQL.
- День 1–2: фильтрация, агрегаты и NULL на маленьких таблицах.
- День 3: JOIN с проверкой количества строк до и после.
- День 4: окна и однозначный порядок событий.
- День 5: конверсия и повторные покупки с заданным знаменателем.
- День 6: несколько задач с объяснением вслух.
- День 7: разбор ошибок и повтор задач без подсказок.
SQL — одна часть подготовки. Дополнительно проверь статистику, Python и продуктовые вопросы. Если нужен разбор твоих решений и повторяющихся ошибок, есть подготовка к собеседованию с ментором.
Для сверки синтаксиса: оконные функции PostgreSQL и агрегатные функции PostgreSQL.
Хочешь проверить, как решаешь SQL-задачи?
Напиши, какие задачи уже решаешь самостоятельно и к какой роли готовишься. На первом бесплатном созвоне обсудим пробелы и подходящий следующий шаг.
Обсудить SQL-подготовку ↗