new-lvl.pro · Статьи · SQL · Собес
Статья // 16 мин чтения

8 SQL-задач,
которые дают на собесе
в BigTech

Top-N в группе, rolling DAU, cohort retention, time-to-event, sessions из событий, перцентили, ratio с доверительным интервалом, anti-fraud. 8 форматов покрывают 90% SQL-собесов middle-уровня в Яндексе, Авито, Ozon, Тинькофф. С решениями, подвохами и тем, что говорить вслух.

Почему именно эти 8 форматов

SQL-секцию на собесе аналитика в российских BigTech обычно строят из 2–4 задач за 45–60 минут. Содержание плавающее, но форматы повторяются. Я разобрал пакет задач из «своих» собесов, из обсуждений с менти после Яндекса, Авито, Ozon, Тинькофф — и получил 8 шаблонов, которые покрывают примерно 90% задач middle-уровня.

Знание именно форматов важнее, чем «много задач решил». Если ты узнал шаблон — пишешь решение уверенно, осталось только аккуратно настроить под детали. Если шаблон не узнал — садишься в код, который компилируется, но даёт неверный ответ.

Решения ниже — на PostgreSQL (тот же диалект, что в Авито, Тинькофф, Ozon, Wildberries и у нас на sql.new-lvl.pro). На MySQL и ClickHouse синтаксис почти идентичный, отличия отмечаю в комментариях, где они принципиальные.

Задача 01
Top-N товаров в категории
window · ROW_NUMBER

Как формулируют

«Есть таблица products(category, name, price). Найди 3 самых дорогих товара в каждой категории.»

Решение

SELECT category, name, price
FROM (
  SELECT category, name, price,
         ROW_NUMBER() OVER (
           PARTITION BY category
           ORDER BY price DESC
         ) AS rn
  FROM products
) t
WHERE rn <= 3;

Подвох

Главный — «у двух товаров одинаковая цена, какой попадёт в топ?». Это вопрос про разницу между ROW_NUMBER, RANK и DENSE_RANK.

ROW_NUMBER — гарантирует ровно 3 строки, но порядок ничьих неопределён.
RANK — при ничьей все получают одинаковый ранг, следующий пропускается (1, 1, 3, 4). На выходе может быть больше 3 строк.
DENSE_RANK — ничьи получают одинаковый ранг, следующий идёт подряд (1, 1, 2, 3). Тоже может вернуть больше 3 строк.

На собесе ожидают, что аналитик уточнит у собеседника: «А что делать с ничьими? Если ровно 3 — это ROW_NUMBER с tie-breaker'ом (например, name). Если можно больше — DENSE_RANK». Просто написать ROW_NUMBER и не объяснить ничьи — это junior-ответ.

// Tie-breaker
Чтобы упорядочить ничьи, добавь второй ключ в ORDER BY: ORDER BY price DESC, name ASC. Без него порядок недетерминированный, и на тех же данных запрос может вернуть разные строки.
Задача 02
Rolling DAU за 7 дней
window · ROWS BETWEEN

Как формулируют

«Есть таблица events(user_id, event_date). Посчитай на каждый день за последний месяц rolling-DAU за 7 дней — количество уникальных пользователей за прошедшие 7 дней.»

Решение (важно — уникальные пользователи)

-- Уникальные пользователи за 7-дневное окно — через self-join
WITH dates AS (
  SELECT DISTINCT event_date
  FROM events
)
SELECT d.event_date,
       COUNT(DISTINCT e.user_id) AS rolling_dau_7d
FROM dates d
JOIN events e
  ON e.event_date BETWEEN d.event_date - INTERVAL '6 days'
                       AND d.event_date
GROUP BY d.event_date
ORDER BY d.event_date;

Подвох

Самая частая ошибка — написать через SUM(dau) OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW). Это не уникальные юзеры за 7 дней, а сумма дневных DAU. Один и тот же юзер, заходивший все 7 дней, посчитается семь раз. На собесе это типичный «троп» — кандидат пишет красивое window-решение, а оно даёт неверный ответ.

Window-функция работает для аддитивных метрик (sum, avg, count без distinct). Для distinct count за окно нужен либо self-join (как выше), либо более тяжёлые приёмы — массивные window-агрегаты в ClickHouse, или approximate count distinct (HyperLogLog).

// Что проверяют
Способность отличить «сумму ежедневных метрик» от «уникальных за период». Это не про синтаксис SQL — это про понимание данных. Сильный middle сразу проговаривает: «через window получим duplicate-count, нужен join».
Задача 03
Cohort retention по месяцам
self-join · cohorts

Как формулируют

«Посчитай retention пользователей по когортам месяца регистрации: на каждую когорту — сколько процентов вернулось в M+1, M+2, M+3 и так далее.»

Решение

WITH cohort AS (
  SELECT user_id,
         DATE_TRUNC('month', MIN(event_date)) AS cohort_m
  FROM events
  GROUP BY user_id
),
activity AS (
  SELECT c.cohort_m,
         DATE_TRUNC('month', e.event_date) AS active_m,
         e.user_id
  FROM cohort c
  JOIN events e USING (user_id)
),
sizes AS (
  SELECT cohort_m, COUNT(DISTINCT user_id) AS cohort_size
  FROM cohort
  GROUP BY cohort_m
)
SELECT
  a.cohort_m,
  EXTRACT(YEAR FROM AGE(a.active_m, a.cohort_m)) * 12 +
  EXTRACT(MONTH FROM AGE(a.active_m, a.cohort_m)) AS months_since,
  COUNT(DISTINCT a.user_id) AS retained,
  COUNT(DISTINCT a.user_id) * 100.0 / s.cohort_size AS retention_pct
FROM activity a
JOIN sizes s ON s.cohort_m = a.cohort_m
GROUP BY a.cohort_m, months_since, s.cohort_size
ORDER BY a.cohort_m, months_since;

Подвох

Часто кандидат забывает делить на cohort_size и оставляет абсолютные числа. Или путает cohort_m с active_m в groupby и получает «когорту последнего возвращения» вместо «когорты регистрации».

Также важно проговорить, что таблица retention — треугольная: свежие когорты успели прожить меньше месяцев, у них вторая половина строк пустая. Это нормально, не баг.

Прорешать эти задачи на нашем датасете
20 free-задач + 80 premium на маркетплейс-датасете 2023–2024. Автопроверка по референс-запросу — можно решать любым способом, главное правильный результат.
Открыть тренажёр
Задача 04
Time-to-event: время до первой покупки
LEAD · разности дат

Как формулируют

«В таблицах users(user_id, signup_at) и orders(user_id, created_at) найди среднее время от регистрации до первой покупки.»

Решение

WITH first_purchase AS (
  SELECT user_id, MIN(created_at) AS first_p
  FROM orders
  GROUP BY user_id
)
SELECT
  AVG(EXTRACT(EPOCH FROM (p.first_p - u.signup_at)) / 86400) AS avg_days,
  PERCENTILE_CONT(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (p.first_p - u.signup_at)) / 86400
  ) AS median_days
FROM users u
JOIN first_purchase p USING (user_id);

Подвох

Главный вопрос — что делать с юзерами, которые не сделали покупку. JOIN их отфильтрует, AVG посчитается только по «купившим». В реальной задаче это часто не то, что хотел продакт: правильный ответ — «среди купивших среднее 12 дней, но 60% базы не купила ни разу». Уточняй у собеседника, какая постановка.

Второй подвох — AVG vs медиана. Time-to-event обычно сильно скошен (большинство покупает в первые 1–3 дня, длинный хвост). Среднее завышено. Медиана и P90 — более содержательные. Сильный middle сразу считает оба.

Задача 05
Сессии из событий: gap > 30 минут — новая сессия
LAG · cumulative SUM

Как формулируют

«В таблице events(user_id, event_at) размечай события по сессиям: если между двумя соседними событиями одного юзера прошло больше 30 минут — это новая сессия.»

Решение

WITH flagged AS (
  SELECT user_id, event_at,
         CASE
           WHEN LAG(event_at) OVER w IS NULL
             OR event_at - LAG(event_at) OVER w
                > INTERVAL '30 minutes'
           THEN 1 ELSE 0
         END AS is_new_session
  FROM events
  WINDOW w AS (PARTITION BY user_id ORDER BY event_at)
)
SELECT user_id, event_at,
       SUM(is_new_session) OVER (
         PARTITION BY user_id
         ORDER BY event_at
       ) AS session_num
FROM flagged
ORDER BY user_id, event_at;

Подвох

Двух-этапная логика: сначала LAG для разметки «новая сессия / нет», потом cumulative SUM по флагу — получаем номер сессии. Кандидат, который пишет это в одном выражении или забывает PARTITION BY user_id во втором OVER, размечает сессии «через юзеров».

Дополнительный вопрос: «А если бы сессии надо было считать вообще по всем юзерам (например, для оценки общей нагрузки)?». Тогда убираем PARTITION BY в обоих OVER, считаем по timestamp и получаем глобальные сессии. Чаще нужны per-user — но проговорить разницу полезно.

Задача 06
Перцентили: медиана и P90 revenue
PERCENTILE_CONT · NTILE

Как формулируют

«В таблице orders посчитай медиану и 90-й перцентиль amount. По дням.»

Решение

SELECT
  DATE(created_at) AS dt,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median,
  PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY amount) AS p90,
  PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY amount) AS p90_disc
FROM orders
GROUP BY dt
ORDER BY dt;

Подвох

Разница между PERCENTILE_CONT и PERCENTILE_DISC:

Функция Что возвращает Когда использовать
PERCENTILE_CONT Интерполированное значение (может не быть в выборке) Стандартный выбор. Дает «гладкие» перцентили.
PERCENTILE_DISC Реальное значение из выборки Когда важно «реально существующее» значение (например, для исторических справок).

Подвох #2 — кандидат может предложить NTILE(10) для перцентилей. NTILE делит на N равных групп, и P90 = «99-я группа из 100». На сегменте 1000 строк всё ровно, на маленькой выборке (10–50) NTILE сильно врёт, потому что округляет. Для точных перцентилей — всегда PERCENTILE_CONT.

Задача 07
Ratio metric с доверительным интервалом
Wald CI · delta-method

Как формулируют

«В таблице sessions(date, user_id, has_purchase) посчитай по дням конверсию из сессий в покупку с 95% доверительным интервалом.»

Решение (Wald-интервал для пропорции)

WITH agg AS (
  SELECT DATE('date') AS dt,
         SUM(has_purchase::int) AS x,
         COUNT(*)::float   AS n
  FROM sessions
  GROUP BY dt
)
SELECT dt,
       x / n AS cr,
       x / n - 1.96 * SQRT((x / n) * (1 - x / n) / n) AS lo,
       x / n + 1.96 * SQRT((x / n) * (1 - x / n) / n) AS hi
FROM agg
ORDER BY dt;

Подвох

Wald-интервал хорошо работает на больших выборках и центральных конверсиях (10–90%). Ломается:

На маленьких выборках (n < 100) — нижняя граница может уйти в минус. Заменяй на Wilson или Clopper-Pearson.
На крайних значениях (CR < 1% или > 99%) — интервал получается асимметричный и Wald врёт. Тот же Wilson или Clopper-Pearson.
На ratio-метриках, которые не пропорции (средний чек = revenue/orders) — Wald вообще неприменим. Нужен delta-method: дисперсия ratio считается как (σ²X / Y²) + (X² σ²Y / Y⁴) − 2·X·Cov(X,Y) / Y³.

На собесе ожидают, что middle различает «конверсия как пропорция» (Wald / Wilson) и «средний чек как ratio» (delta-method). Это и есть проверка глубины понимания статистики.

// Часто подбрасывают подвох
«А если у тебя ratio = revenue / DAU, и DAU и revenue — обе случайные? Какая будет дисперсия?». Ответ: delta-method, нельзя считать ratio как обычную метрику, нужна формула с ковариацией.
Задача 08
Anti-fraud: подозрительный паттерн кликов
пороги · перцентили

Как формулируют

«В таблице events(user_id, event_at, event_type) найди пользователей с подозрительным паттерном: слишком много кликов в короткий период.»

Решение (пороговая логика)

-- Юзеры, у которых было > 100 кликов за любые 10 минут
WITH buckets AS (
  SELECT user_id,
         DATE_TRUNC('minute', event_at)
           - INTERVAL '1 minute'
             * (EXTRACT(MINUTE FROM event_at)::int % 10) AS bucket_10min,
         COUNT(*) AS clicks
  FROM events
  WHERE event_type = 'click'
  GROUP BY user_id, bucket_10min
)
SELECT DISTINCT user_id
FROM buckets
WHERE clicks > 100;

Альтернатива через перцентили

-- Юзеры выше 99-го перцентиля по кликам/день
WITH daily AS (
  SELECT user_id, DATE(event_at) AS dt, COUNT(*) AS clicks
  FROM events
  WHERE event_type = 'click'
  GROUP BY user_id, dt
),
threshold AS (
  SELECT PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY clicks) AS p99
  FROM daily
)
SELECT DISTINCT d.user_id
FROM daily d, threshold t
WHERE d.clicks > t.p99;

Подвох

Главный вопрос — пороговая константа vs относительный перцентиль. Константа («> 100 кликов») — простая, но привязана к продукту: что нормально на Facebook — фрод на B2B-портале. Перцентильный подход универсальнее, но даёт 1% подозреваемых всегда, даже если фрода нет.

В продакшене обычно используют комбинацию: (пороговый «явный фрод-уровень») OR (перцентиль 99.5 + дополнительные признаки). На собесе достаточно проговорить trade-off и предложить одну из реализаций.

Дополнительный пласт — обозначить, что одна метрика не ловит фрод полностью. В реальности добавляют скорость кликов (clicks/сек), географию (IP), user-agent, время суток, и моделируют через ансамбль признаков. На SQL-собесе это не требуется — но упомянуть это в одном предложении показывает зрелость.

Что делать, чтобы пройти SQL-секцию

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

Шаг Действие Сколько времени
1 Прорешать каждый из 8 форматов на 3 разных датасетах ~24 задачи, 1–2 недели вечерами
2 Объяснить вслух (себе, ментору, в зеркало) логику каждой задачи 10 мин на задачу × 24
3 Один mock-интервью с человеком — он задаёт, ты пишешь и объясняешь 1 сессия 45 мин
4 Прорешать 5 «новых» — задач, которых не видел, в формате собеса ~3 часа за неделю до собеса

Главное на собесе — не «написал и молча сидишь». Собеседник оценивает процесс мышления. Уточняй постановку, проговаривай шаги, оговаривай альтернативы. Сильный middle не идеален в синтаксисе — он структурно думает и аккуратно проверяет крайние случаи. Junior пишет код быстрее, но без оговорок.

100 задач на маркетплейс-датасете
На sql.new-lvl.pro — 20 free-задач сразу, без email-стен. Плюс 80 premium по всем форматам, что разобраны выше: top-N, rolling, retention, sessions, ratio, anti-fraud. Автопроверка по референс-запросу — можно решать любым способом.
Решить 100 задач

Частые вопросы

Сколько SQL-задач реально дают на собесе в BigTech?
Обычно 2–4 за SQL-секцию (45–60 мин). На грейд junior — 1–2 простые (агрегация, фильтрация, простой join). На middle — 2–3 средних (одна из 8 форматов выше). На senior — 3–4, минимум одна «закаленная» (сессии из событий, ratio с CI, anti-fraud). Иногда задачи цепляются друг за друга — сначала простая, потом усложнение.
На каком диалекте обычно собеседуют?
В российских BigTech — почти всегда PostgreSQL или его диалект. Авито, Тинькофф, Ozon, Wildberries, Yandex Cloud — все на Postgres. ClickHouse часто упоминают, но на собесе используют редко — слишком много специфики. MySQL встречается у западных компаний и в e-com сегменте. Готовься на PostgreSQL — этот синтаксис покрывает 90% случаев.
Что важнее на собесе — синтаксис или объяснение?
Объяснение. Собеседник видит, что ты пишешь, но оценивает как ты к этому пришёл. Сильный middle проговаривает: «уточню постановку, есть ли ничьи; начну с CTE для агрегатов; добавлю window для ранжирования; проверю крайние случаи на NULL». Это и есть структурное мышление. Если ты молча пишешь — даже идеальный код может оцениваться ниже, чем «слегка корявый, но с правильной логикой и оговорками».
Какие подвохи ловят чаще всего?
Топ-3: (1) COUNT DISTINCT за окно через window-функцию — даёт duplicate-count, не уникальных; (2) Ratio считают как простую метрику, забывая про delta-method; (3) Cohort retention без деления на размер когорты — выходят абсолютные числа вместо процентов. На любом из этих трёх кандидат теряет 20–30% оценки SQL-секции.
Если я застрял — лучше молчать или признаться?
Признаться вслух: «Я знаю, что нужен window-функция для top-N в группе, но забыл точный синтаксис ORDER BY внутри OVER. Можно подсмотреть документацию?». Это лучше, чем молчать 10 минут, или писать наугад. Собеседники в BigTech уважают честность плюс понимание структуры. Они хотят увидеть, как ты решаешь незнакомую задачу, а не «энциклопедию синтаксиса».
Стоит ли учить PERCENTILE_CONT, если NTILE проще?
Да. PERCENTILE_CONT даёт точные перцентили, NTILE — приближенные (на маленьких выборках сильно врёт). На собесе в Тинькофф/Авито/Yandex одна из задач — «найди медиану». Ответ «NTILE(2)» считается слабым: средняя группа из двух — это не медиана. Учить пять минут, использовать всегда.

Связанные материалы

Главное про SQL-собес в BigTech

Восемь форматов выше покрывают примерно 90% задач middle-уровня. Не все собесы делают по этим шаблонам — на senior могут добавить approximate distinct, recursive CTE для иерархий, что-то экзотическое. Но 90% — это и есть нормальный целевой purchase: если уверенно решаешь восемь, средний собес проходишь.

Запомни три ориентира: (1) формат важнее синтаксиса — узнал задачу = решил; (2) проговаривай вслух уточнения постановки, ничьи, NULL'ы, крайние случаи — это половина оценки; (3) моторика бьётся 50+ задачами на разных датасетах, не одним прочтением. Шаблон без практики — это всё ещё junior.

Следующий шаг — открой sql.new-lvl.pro, прорешай 5 free-задач на window-функции и одну на retention. Это два самых частых формата на собесе. Один-два вечера — и SQL-секция перестанет быть страшной.

АТ
Андрей Тарасенко
// Продуктовый аналитик · Авито · Ментор

Самая частая моя реплика менти после плохого SQL-собеса: «ты знал, как это решить, просто не узнал шаблон». Если научиться распознавать восемь форматов в формулировках собеседника — синтаксис подтягивается за выходные. Решение это вторично — узнавание шаблона первично.

Написать в Telegram
// ЗАКРЕПИ НА ПРАКТИКЕ

Прочитал теорию? Теперь реши задачу

В SQL-тренажёре — реальная база маркетплейса и задачи по мотивам собесов BigTech. 20 задач бесплатно, решение проверяется мгновенно.

▶ Открыть SQL-тренажёр ★ Premium · 80 задач
Все материалы: База знаний · Telegram