SQL на собеседовании аналитика: 20 задач от SELECT до когорт
Практический разбор SQL-задач с собеседований: JOIN, GROUP BY, оконные функции, даты, retention, воронки и проверки, которые отличают рабочий запрос от случайного ответа.
Содержание статьи
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 человек.
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) почти никогда не является исправлением: два разных заказа могут иметь одинаковую сумму.
| Шаг | Ожидаемое зерно | Проверка |
|---|---|---|
| users | одна строка на user_id | unique user_id = rows |
| orders | одна строка на order_id | unique order_id = rows |
| order_items | одна строка на товар в заказе | order_id может повторяться |
| items_to_order | одна строка на order_id | group by order_id перед JOIN |
| итог | одна строка на страну | ключ страны уникален |
Задачи 8–10: топ-N и оконные функции
Топ-3 товара в каждой категории, первая покупка пользователя и накопительная выручка — классические задачи на row_number, rank, lag и sum() over. Не путайте row_number и rank: первый всегда даёт уникальный порядок, второй сохраняет одинаковый ранг при равных значениях. Выбор зависит от того, должен ли ответ содержать ровно N строк или всех участников с одинаковым результатом.
Перед оконной функцией почти всегда нужен промежуточный слой. Сначала агрегируйте до товара, пользователя или дня. Затем ранжируйте. Если поставить окно над сырыми позициями, оно ответит на другой вопрос и будет сложнее проверяться. В примере первая покупка — первая оплата в payments: с rn = 1 остаётся 912 строк, по одной на плательщика, запрос показывает десять самых ранних.
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 человек отправили приглашение без отчёта, и в последовательном режиме числа были бы меньше.
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.
Для реальной скорости полезнее десять задач с разбором ошибок, чем пятьдесят задач, решённых молча. После каждого раунда записывайте, где ошиблись: в синтаксисе, зерне, периоде, знаменателе или трактовке. Эти категории дают понятный план подготовки.
Материалы по теме
SQL-задачи с решениями: 30 задач для аналитика на данных продукта
Тридцать SQL-задач с ответами и решениями на одной учебной базе: WHERE, GROUP BY, JOIN, даты, когорты и оконные функции. Каждый ответ можно проверить в песочнице.
Вопросы по SQL на собеседовании: 35 вопросов с короткими ответами
Теоретические вопросы по SQL для аналитика с ответами в два-пять предложений: ключи, NULL, JOIN, GROUP BY, окна, индексы, CTE, DAU и retention — с примерами на данных.

LEFT JOIN в SQL: отличие от INNER JOIN, NULL и условие в ON
LEFT JOIN в SQL на примерах: что возвращает, чем отличается от INNER JOIN, почему условие в WHERE теряет строки и как найти записи без пары.