Все материалы
Продуктовая аналитикапрактикумсредний

SQL на собеседовании аналитика: 20 задач от SELECT до когорт

Практический разбор SQL-задач с собеседований: JOIN, GROUP BY, оконные функции, даты, retention, воронки и проверки, которые отличают рабочий запрос от случайного ответа.

КейсПрактика13 августа 2026 г.обновлено 2 октября 2026 г.9 мин

SQL-задача на собеседовании проверяет не количество выученных конструкций. Важнее, понимаете ли вы, сколько строк лежит в таблице, где появляется дубликат, какой знаменатель нужен и как убедиться, что результат не случайно размножился после JOIN. Ниже — набор задач, который удобно проходить как тренировочный маршрут: от простого фильтра до когорт и окон. Условия записаны на типовой схеме интернет-магазина (users, orders, order_items), а три запроса с кодом — на учебной базе симулятора: заказов в ней нет, их роль играют оплаты, зато запросы выполняются в песочнице как есть. Для следующего уровня есть 12 задач middle-аналитика на учебной базе авиакомпании.

Как начать SQL-задачу

Первые тридцать секунд лучше потратить на уточнение условий. Спросите, что означает одна строка, какие статусы считать валидными, какой период нужен, что делать с NULL и должен ли результат содержать пользователей без события. Затем назовите ожидаемое зерно: «одна строка на пользователя» или «одна строка на месяц и канал». Это сразу задаёт форму решения.

Если условие неполное, зафиксируйте допущение вслух и продолжите. Интервьюер обычно оценивает, заметил ли кандидат неоднозначность. Гораздо хуже молча выбрать случайный знаменатель и получить технически красивый, но бесполезный результат.

  • Назвать зерно исходных таблиц и результата.
  • Проверить ключи JOIN и ожидаемую кардинальность.
  • Отделить фильтр периода от фильтра статуса.
  • Решить, нужны ли пользователи без событий.
  • После запроса проверить количество строк и дубли ключа.

Задачи 1–4: SELECT, фильтры и агрегация

На старте часто дают таблицу orders и просят найти оплаченные заказы за месяц, выручку по каналам, покупателей с двумя и более заказами или долю отмен. Эти вопросы кажутся простыми, но проверяют полуинтервал дат, COUNT DISTINCT, деление без целочисленного округления и правильный уровень агрегации. Запрос ниже переносит те же приёмы на оплаты: платежи июля по тарифам, плательщики, выручка и доля продлений.

Говорите, почему считаете заказы через count(distinct order_id), если таблица может содержать позиции. Если статус относится к заказу, а строка — к товару, фильтрация после JOIN с products не должна менять бизнес-смысл. В идеале сначала приведите данные к нужному зерну, затем считайте метрику. В примере так же разведены платежи и плательщики: на тарифе basic в июле 246 платежей от 243 человек.

sqlПлатежи, плательщики, выручка и доля продлений по тарифам за июль
select plan,
  count(*) as payments,
  count(distinct user_id) as payers,
  sum(amount) as revenue,
  count(*) filter (where payment_type = 'renewal') as renewals,
  round(cast(
    1.0 * count(*) filter (where payment_type = 'renewal')
    / nullif(count(*), 0) as numeric), 3
  ) as renewal_share
from payments
where paid_at >= date '2026-07-01'
  and paid_at < date '2026-08-01'
group by plan
order by revenue desc nulls last, plan;

Задачи 5–7: JOIN без размножения строк

Сценарий: есть users, orders и order_items. Нужно показать число пользователей, заказы и выручку по стране. Самая частая ошибка — соединить users с items и посчитать пользователей через count(*), а затем удивиться завышенной цифре. До запроса скажите, что items имеет зерно «одна позиция», orders — «один заказ», а итог должен быть на стране.

Для денежных расчётов сначала полезно агрегировать позиции до заказа, а уже затем присоединять пользователей. Если это невозможно, используйте count(distinct) и проверьте, что сумма не дублируется. SUM(DISTINCT amount) почти никогда не является исправлением: два разных заказа могут иметь одинаковую сумму.

Контроль кардинальности JOIN
ШагОжидаемое зерноПроверка
usersодна строка на user_idunique user_id = rows
ordersодна строка на order_idunique order_id = rows
order_itemsодна строка на товар в заказеorder_id может повторяться
items_to_orderодна строка на order_idgroup by order_id перед JOIN
итогодна строка на странуключ страны уникален

Задачи 8–10: топ-N и оконные функции

Топ-3 товара в каждой категории, первая покупка пользователя и накопительная выручка — классические задачи на row_number, rank, lag и sum() over. Не путайте row_number и rank: первый всегда даёт уникальный порядок, второй сохраняет одинаковый ранг при равных значениях. Выбор зависит от того, должен ли ответ содержать ровно N строк или всех участников с одинаковым результатом.

Перед оконной функцией почти всегда нужен промежуточный слой. Сначала агрегируйте до товара, пользователя или дня. Затем ранжируйте. Если поставить окно над сырыми позициями, оно ответит на другой вопрос и будет сложнее проверяться. В примере первая покупка — первая оплата в payments: с rn = 1 остаётся 912 строк, по одной на плательщика, запрос показывает десять самых ранних.

sqlПервая оплата пользователя через ROW_NUMBER
with ranked_payments as (
  select user_id, payment_id, paid_at, amount,
    row_number() over (
      partition by user_id
      order by paid_at, payment_id
    ) as rn
  from payments
)
select user_id, payment_id, paid_at, amount
from ranked_payments
where rn = 1
order by paid_at, payment_id
limit 10;

Задачи 11–13: даты, NULL и границы периода

Вопросы про «прошлый месяц», «активность на следующий день» и «пользователей без покупки» выявляют зрелость быстрее, чем редкий синтаксис. Используйте явную таймзону, полуинтервалы и coalesce только там, где неизвестное действительно нужно превратить в ноль. NULL в цене, канале и дате означает разные вещи и не должен обрабатываться одной универсальной заменой.

Для активности на D7 сначала зафиксируйте дату старта когорты, а затем присоедините событие ровно в нужное окно. Не вычитайте даты без понимания часового пояса: событие в 23:30 UTC может попасть уже в следующий календарный день бизнеса.

  • Правая граница периода обычно не включается.
  • Таймзона должна быть одинаковой в cohort и activity.
  • LEFT JOIN нужен, если отсутствие события — важный результат.
  • NULL не равен нулю и не равен пустой строке.
  • CURRENT_DATE удобен для примера, но контрольную дату лучше параметризовать.

Задачи 14–16: retention и воронка

Для retention создайте cohorts с user_id и cohort_day, затем activity с уникальной парой user_id и active_day. После этого посчитайте distinct пользователей на D1, D7 или D30 и разделите на размер исходной когорты. Главная проверка — D7 не должен случайно считаться от пользователей, которые уже вернулись на D1.

Для воронки лучше собрать user-level flags: создал ли пользователь workspace и отчёт, отправил ли приглашение, сделал ли экспорт. Так один пользователь не размножается из-за нескольких событий, а шаги можно сравнить от одной стартовой базы или в последовательном режиме — в зависимости от условия. На учебной базе из 1 676 зарегистрированных в июле workspace создали 951, отчёт — 612, приглашение отправили 418, экспорт сделали 335. Это счёт от стартовой базы: 266 человек отправили приглашение без отчёта, и в последовательном режиме числа были бы меньше.

sqlUser-level flags для воронки июльской когорты
with flags as (
  select u.user_id,
    bool_or(e.event_name = 'workspace_created') as created_workspace,
    bool_or(e.event_name = 'report_created') as created_report,
    bool_or(e.event_name = 'invite_sent') as sent_invite,
    bool_or(e.event_name = 'export_completed') as exported
  from users as u
  left join events as e on e.user_id = u.user_id
  where u.signup_date >= date '2026-07-01'
    and u.signup_date < date '2026-08-01'
  group by u.user_id
)
select
  count(*) as signups,
  count(*) filter (where created_workspace) as created_workspace,
  count(*) filter (where created_report) as created_report,
  count(*) filter (where sent_invite) as sent_invite,
  count(*) filter (where exported) as exported
from flags;

Задачи 17–18: доля, медиана и контрольный ответ

Если просят долю пользователей с повторной покупкой или медианное время до активации, уточните знаменатель и способ обработки пользователей без результата. Для медианы не подставляйте ноль тем, кто не активировался: это смешает время успешных пользователей с долей неуспеха. Сначала покажите activation rate, затем распределение time-to-value среди активированных.

После запроса назовите контрольный ответ. Например, число строк должно равняться числу каналов, дата первой покупки не может быть раньше регистрации, а сумма выручки после JOIN должна совпадать с независимой агрегацией orders. Такая финальная проверка превращает решение из «код выполнился» в аналитический результат.

Задачи 19–20: качество и производительность

В продвинутом раунде могут спросить, как найти дубли, пропуски, нарушение уникальности или медленный запрос. Начните с бизнес-инварианта: order_id должен быть уникален, сумма платежей не должна быть отрицательной без refund, событие не должно лежать раньше даты регистрации. Затем выберите SQL-проверку и обсудите, где её запускать — в модели, тесте или мониторинге.

Производительность объясняйте через план и объём, а не через магическое «добавлю индекс». Сначала отфильтруйте период, проверьте кардинальность JOIN, уберите ненужные колонки, посмотрите EXPLAIN и только потом обсуждайте индекс или предварительную агрегацию. В аналитической базе правильный partition pruning может дать больше, чем локальная перестановка CTE.

Чего не стоит делать на собеседовании

Не начинайте с длинного запроса, не проверив условие. Не маскируйте дубликаты DISTINCT без объяснения. Не говорите «запрос правильный», пока не сверили зерно, количество строк и крайние случаи.

Скрытые проверки в SQL-задаче

Интервьюер может не сказать, что в таблице есть повторные события, NULL или пользователи без заказов. Это не ловушка ради ловушки: в реальной работе именно такие детали меняют ответ. Поэтому после основного запроса добавьте короткий аудит. Сколько строк в источнике? Уникален ли ключ? Какова доля пропусков? Сколько пользователей потерял INNER JOIN? Совпадает ли сумма после соединения с исходной таблицей?

Если времени мало, выберите одну критичную проверку и скажите, какую выполните после интервью. Важно показать приоритет. Для денежного отчёта это будет сверка суммы и возвратов. Для retention — зрелость когорты и таймзона. Для воронки — distinct user_id и порядок шагов.

Проверка по типу задачи
ЗадачаГлавный рискБыстрая проверка
Выручкаразмножение позиций и возвратысверить сумму на уровне order_id
Retentionнезрелые когорты и неверный return eventпоказать cohort size и max age
Воронкасобытия вместо пользователейсобрать user-level flags
Top-Nранжирование до агрегациипроверить зерно перед окном

Как объяснять сложность и компромиссы

На middle- и senior-уровне могут спросить, как ускорить запрос. Не обещайте, что CTE, индекс или переписывание в подзапрос всегда решит проблему. Сначала оцените объём, селективность фильтра, план JOIN и размер промежуточных наборов. В warehouse важно не скачать лишние строки, не потерять partition pruning и не считать одну тяжёлую агрегацию заново в нескольких виджетах.

Если есть два корректных варианта, назовите trade-off. Оконная функция может быть читаемее, а предварительная агрегация — дешевле на большом объёме. Временная таблица может упростить отладку, но добавить поддержку. Интервьюеру важно услышать, что выбор зависит от масштаба, SLA и стоимости сопровождения.

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

Как тренировать SQL вслух

Возьмите одну задачу и решите её в три прохода. В первом только проговорите таблицы, зерно и план. Во втором напишите минимальный запрос. В третьем добавьте проверку и расскажите, как изменится запрос при новом условии. Например, после топ-3 по категории попросите топ-3 по каждой неделе и обсудите, что меняется в PARTITION BY.

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

Продолжить чтение
Вся библиотека