SQL на собеседовании аналитика: 6 задач с решениями

SQL-задачи для собеседования аналитика данных и продуктового аналитика: JOIN, NULL, конверсия, оконные функции и повторные покупки. Решения и типичные ошибки.

Никита Моисеев · по материалам платформы «Путь» · Обновлено

На SQL-секции важно получить корректный результат и объяснить логику запроса. Даже короткая задача может проверять гранулярность данных, дубли после JOIN, знаменатель конверсии и смысл оконной функции.

В статье: шесть авторских учебных задач с решениями для PostgreSQL, ошибки в расчётах и план самопроверки. Задачи адаптированы под темы платформы «Путь» и не приписываются конкретным работодателям.

Какие темы SQL подготовить аналитику

Перед запросом уточни: что означает одна строка, какой период нужен, какой статус считать покупкой и как учитывать возвраты. В примерах ниже 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) не исправляет ошибку: два разных заказа могут иметь одинаковую стоимость.

Как решать задачу вслух на собеседовании

  1. Перескажи требуемый результат: какой объект должен быть в одной строке ответа.
  2. Уточни ключи, время, статусы, пропуски и повторы.
  3. Раздели решение на шаги: отбор → соединение → агрегация → окно.
  4. Проверь на маленьком примере, включая пустую выборку и несколько заказов одного человека.
  5. Объясни ограничения и то, что изменишь при другом условии.

Незнакомая функция не отменяет понимания задачи. Сначала объясни логику, затем уточни доступный SQL-диалект. В ClickHouse некоторые агрегаты, даты и поведение JOIN отличаются от PostgreSQL; конкретный запрос нужно проверять в выбранной СУБД.

План подготовки по SQL на неделю

Это цикл самопроверки для человека, который уже знаком с основами SQL.

SQL — одна часть подготовки. Дополнительно проверь статистику, Python и продуктовые вопросы. Если нужен разбор твоих решений и повторяющихся ошибок, есть подготовка к собеседованию с ментором.

Для сверки синтаксиса: оконные функции PostgreSQL и агрегатные функции PostgreSQL.

Хочешь проверить, как решаешь SQL-задачи?

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

Обсудить SQL-подготовку ↗