тестовое заданиеSQLсобеседованиеаналитик данныхкогортный анализ

Тестовое задание аналитика: 4 задачи с решением на SQL

2026-07-10 14 мин

Коротко: Разбираем настоящее тестовое задание аналитика данных на маркетплейсе: один датасет (заказы, пользователи, события) и четыре задачи — от выручки по месяцам до продуктового вопроса «почему упал GMV». К каждой — условие, ход мысли, готовый SQL на PostgreSQL и частые ошибки, из-за которых тестовое не засчитывают. Все запросы можно тут же прогнать в SQL-тренажёре.

Take-home (тестовое на дом) — самый частый формат отбора аналитика после SQL-скрининга. Компания присылает схему таблиц и 3–5 задач, вы возвращаете запросы и короткие выводы. Оценивают не только «работает ли SQL», но и корректность на краевых случаях, читаемость и способность объяснить результат бизнесу. Ниже — типичное задание маркетплейса целиком, с решениями, которые не стыдно отправить.

Как выглядит датасет в тестовом задании?

Три таблицы — упрощённо, но близко к реальному take-home:

-- пользователи
users(user_id, signup_date, city, channel)
-- заказы (status: 'paid' | 'cancelled' | 'refunded')
orders(order_id, user_id, created_at, status, amount)
-- события воронки (event_name: 'view' | 'add_to_cart' | 'checkout')
events(event_id, user_id, event_ts, event_name)

Первое, что делает сильный кандидат, — задаёт себе вопросы к данным ещё до запросов:

Эти три оговорки в комментарии к решению часто важнее, чем сам SQL: они показывают, что вы думаете про данные, а не просто пишете SELECT.

Задача 1. Как посчитать выручку и средний чек по месяцам с ростом MoM?

Условие: по месяцам вывести число оплаченных заказов, выручку, средний чек и прирост выручки к предыдущему месяцу в процентах (month-over-month).

Подход: группируем по date_trunc('month', ...), берём только оплаченные заказы, а MoM считаем оконной функцией LAG поверх уже агрегированной выручки.

SELECT
  date_trunc('month', created_at)::date AS month,
  count(*)              AS orders,
  sum(amount)           AS revenue,
  round(avg(amount), 2) AS avg_check,
  round(
    (sum(amount) - lag(sum(amount)) OVER (ORDER BY date_trunc('month', created_at)))
    / nullif(lag(sum(amount)) OVER (ORDER BY date_trunc('month', created_at)), 0) * 100,
    1
  ) AS mom_growth_pct
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;

Разбор: nullif(..., 0) защищает от деления на ноль в первом месяце (там LAG вернёт NULL — и это правильно, роста считать не от чего). Оконная функция применяется уже после GROUP BY, поэтому lag(sum(amount)) берёт выручку предыдущей строки-месяца. Про нарастающие расчёты подробнее — в гайде про running total.

Частые ошибки: считают выручку по всем статусам (завышают GMV возвратами); делят на ноль в первом месяце; путают avg(amount) (средний чек) с sum/count вручную — результат тот же, но проверяющий любит явный avg.

Задача 2. Как построить воронку конверсии view → корзина → оплата?

Условие: посчитать, сколько уникальных пользователей прошли каждый шаг (просмотр → добавление в корзину → checkout) и конверсию между шагами.

Подход: считаем уникальных пользователей на каждом шаге через COUNT(DISTINCT user_id) FILTER (...) — это одна проходка по таблице вместо трёх подзапросов.

WITH funnel AS (
  SELECT
    count(DISTINCT user_id) FILTER (WHERE event_name = 'view')        AS viewers,
    count(DISTINCT user_id) FILTER (WHERE event_name = 'add_to_cart') AS carted,
    count(DISTINCT user_id) FILTER (WHERE event_name = 'checkout')    AS checked_out
  FROM events
)
SELECT
  viewers, carted, checked_out,
  round(carted::numeric      / nullif(viewers, 0) * 100, 1) AS view_to_cart_pct,
  round(checked_out::numeric / nullif(carted, 0)  * 100, 1) AS cart_to_checkout_pct,
  round(checked_out::numeric / nullif(viewers, 0) * 100, 1) AS overall_pct
FROM funnel;

Разбор: FILTER (WHERE ...) — чистый способ посчитать несколько метрик по разным условиям в одном проходе (в старых диалектах пишут count(DISTINCT CASE WHEN ... THEN user_id END)). Приведение ::numeric обязательно — иначе целочисленное деление даст 0. Полный разбор воронок — в статье про воронку конверсии в SQL.

Частые ошибки: считают COUNT(*) вместо COUNT(DISTINCT user_id) — один пользователь с тремя просмотрами раздувает верх воронки; забывают ::numeric и получают нули; считают конверсию шага от общего числа вместо предыдущего шага.

Задача 3. Как посчитать retention покупателей по когортам?

Условие: для каждой месячной когорты (месяц первого оплаченного заказа) показать, сколько пользователей вернулись за покупкой через 1, 2, 3… месяца.

Подход: сначала определяем когорту каждого пользователя (месяц первого заказа), затем — все месяцы его активности, и считаем разницу в месяцах между активностью и когортой.

WITH first_order AS (
  SELECT user_id,
         date_trunc('month', min(created_at))::date AS cohort_month
  FROM orders
  WHERE status = 'paid'
  GROUP BY user_id
),
activity AS (
  SELECT DISTINCT user_id,
         date_trunc('month', created_at)::date AS active_month
  FROM orders
  WHERE status = 'paid'
)
SELECT
  f.cohort_month,
  (extract(year  FROM a.active_month) - extract(year  FROM f.cohort_month)) * 12
  + (extract(month FROM a.active_month) - extract(month FROM f.cohort_month)) AS month_number,
  count(DISTINCT a.user_id) AS retained_users
FROM first_order f
JOIN activity a ON a.user_id = f.user_id
GROUP BY f.cohort_month, month_number
ORDER BY f.cohort_month, month_number;
month_number = 0 — это размер когорты (100%), остальные строки делятся на него, чтобы получить процент удержания. Подробный разбор с матрицей ретеншена — в статье про когортный анализ.

Частые ошибки: считают retention от календарного месяца, а не от месяца когорты (тогда «месяц 1» у всех разный); забывают DISTINCT в активности и ловят дубли при нескольких заказах в месяце; берут signup_date вместо месяца первой покупки — это уже другой ретеншен (регистрация, а не удержание покупателей).

Задача 4. GMV упал на 12% за месяц — как найти причину?

Это продуктовый вопрос, и SQL здесь — инструмент, а не ответ. Оценивают структуру мышления, а не один запрос. Сильный ответ идёт по декомпозиции:

Диагностический запрос — разложить изменение выручки по каналам, чтобы увидеть, где именно утекло:

SELECT
  u.channel,
  sum(o.amount) FILTER (WHERE date_trunc('month', o.created_at) = date '2026-02-01') AS feb,
  sum(o.amount) FILTER (WHERE date_trunc('month', o.created_at) = date '2026-03-01') AS mar
FROM orders o
JOIN users u USING (user_id)
WHERE o.status = 'paid'
GROUP BY u.channel
ORDER BY mar - feb;  -- сверху каналы с самым большим падением

Вывод формулируй как гипотезу с числом и рекомендацией: не «выручка упала», а «80% падения GMV пришлось на платный трафик из-за роста CPO — стоит проверить ставки в марте». Фреймворк диагностики метрики разобран в отдельной статье, а формула самой метрики — на странице GMV.

Что на самом деле оценивают в тестовом задании?

Проверяющий смотрит не только на правильный ответ, но и на инженерную аккуратность:

Хорошее правило: к каждому запросу — две-три строки текста с выводом. Это отличает junior-решение («вот SQL») от middle («вот SQL, вот что я вижу, вот что бы проверил дальше»).

Где потренироваться на таких заданиях?

Одно разобранное тестовое — это старт, но навык ставится объёмом. На zasqlpython.ru собрано 600+ тестовых заданий по SQL, Python, A/B-тестам и продукту, а также 545 SQL-задач в тренажёре с автопроверкой прямо в браузере и разбором каждой темы в бесплатном курсе SQL с нуля.

Начни с каталога тестовых заданий и SQL-тренажёра — первые задачи открыты без регистрации. А потренировать себя на реальном собеседовании поможет AI-интервьюер: он задаёт вопросы, слушает ответы и даёт обратную связь как настоящий интервьюер.

Материал подготовлен командой тренажёра «Заскуль Питона» (zasqlpython.ru) — практика SQL, Python и продуктовой аналитики для подготовки к собеседованиям.

Реши тестовые задания с автопроверкой
600+ тестовых по SQL, Python, A/B и продукту + 545 SQL-задач в тренажёре. Первые — без регистрации.
Открыть тестовые задания →