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

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

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

КПКейсПрактика13 августа 2026 г.28 мин

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

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

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

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

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

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

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

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

sqlДоля отмен и выручка по каналам
select channel,
  count(distinct order_id) filter (where status = 'paid') as paid_orders,
  count(distinct order_id) filter (where status = 'cancelled') as cancelled_orders,
  sum(amount) filter (where status = 'paid') as revenue,
  round(
    count(distinct order_id) filter (where status = 'cancelled')::numeric
    / nullif(count(distinct order_id), 0), 3
  ) as cancel_rate
from orders
where created_at >= date '2026-07-01'
  and created_at < date '2026-08-01'
group by channel
order by revenue desc nulls last;

Задачи 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 строк или всех участников с одинаковым результатом.

Перед оконной функцией почти всегда нужен промежуточный слой. Сначала агрегируй до товара, пользователя или дня. Затем ранжируй. Если поставить окно над сырыми позициями, оно ответит на другой вопрос и будет сложнее проверяться.

sqlПервая покупка пользователя через ROW_NUMBER
with ranked_orders as (
  select user_id, order_id, created_at, amount,
    row_number() over (
      partition by user_id
      order by created_at, order_id
    ) as rn
  from orders
  where status = 'paid'
)
select user_id, order_id, created_at, amount
from ranked_orders
where rn = 1;

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

sqlUser-level flags для воронки
select user_id,
  bool_or(event_name = 'pricing_viewed') as saw_pricing,
  bool_or(event_name = 'signup_completed') as signed_up,
  bool_or(event_name = 'workspace_created') as created_workspace,
  bool_or(event_name = 'report_viewed') as saw_report
from events
where occurred_at >= timestamp '2026-07-01'
  and occurred_at < timestamp '2026-08-01'
group by user_id;

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

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

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

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

В продвинутом раунде могут спросить, как найти дубли, пропуски, нарушение уникальности или медленный запрос. Начни с бизнес-инварианта: order_id должен быть уникален, сумма платежей не должна быть отрицательной без refund, событие не должно лежать до signup_at. Затем выбери 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.

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

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