SQL на собеседовании аналитика: что спрашивают
users и orders по user_id нужен?Почему SQL важен
SQL — самый частый практический вопрос на собесах аналитика. В 90% собеседований в tech на middle+ уровне есть SQL-секция.
Даже если в работе вы используете Python/pandas больше, SQL остаётся универсальным языком взаимодействия с данными. От SQL на собесе зависит, будет ли оффер.
Хорошая новость: репертуар задач ограничен. 20-30 паттернов покрывают 90% вопросов.
Формат SQL-секции
Варьируется:
Live coding. Решаете задачу в онлайн-редакторе (LeetCode, HackerRank, собственный) с демонстрацией экрана. 30-45 минут, 2-3 задачи.
Whiteboard (устно). На доске / в чате Zoom пишете решения. Интервьюер оценивает логику больше, чем синтаксис.
Take-home. Даётся 2-24 часа на решение сложной задачи. Реже, но бывает.
В любом формате принцип одинаков: понять задачу, думать вслух, проверить решение.
Типы задач
1. Фильтрация + агрегация. Базовый уровень.
«Найди топ-10 клиентов по сумме покупок за последний год».
2. Join-ы. Несколько таблиц.
«Покажи пользователей и их последнюю покупку».
3. Оконные функции. Middle+.
«Для каждого пользователя второй заказ», «running total», «ранк внутри группы».
4. Работа с датами.
«DAU за последние 30 дней», «пользователи, вернувшиеся на Day 7».
5. CTE и подзапросы.
Структурирование сложных запросов.
6. Self-join-ы.
«Пары последовательных покупок», «менеджеры и подчинённые».
7. Пивоты.
«Распредели по столбцам: платформы или категории».
8. Когорты и retention.
«Retention по когортам регистрации на Day N».
Задача: Top N per group
Классика.
«Для каждого отдела найди 2 самых высокооплачиваемых сотрудника».
Решение через ROW_NUMBER():
WITH ranked AS (
SELECT
department,
name,
salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS rn
FROM employees
)
SELECT department, name, salary
FROM ranked
WHERE rn <= 2;Вариации:
- RANK() если равные значения должны получить одинаковый ранг.
- DENSE_RANK() если после равных рангов не пропускать значения.
Задача: Running total
«Посчитай накопительную сумму выручки по дням».
SELECT
DATE,
revenue,
SUM(revenue) OVER (ORDER BY DATE) AS cumulative_revenue
FROM daily_revenue
ORDER BY DATE;Окно без PARTITION — по всем строкам.
Для накопления по группам:
SUM(revenue) OVER (PARTITION BY customer_id ORDER BY DATE)Задача: DAU и MAU
«Посчитай DAU на каждый день за последний месяц».
SELECT
DATE(event_time) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_time >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(event_time)
ORDER BY day;MAU rolling: 30-дневное окно на каждый день.
WITH days AS (
SELECT DISTINCT DATE(event_time) AS day
FROM events
WHERE event_time >= CURRENT_DATE - INTERVAL '60 days'
)
SELECT
d.day,
(SELECT COUNT(DISTINCT e.user_id)
FROM events e
WHERE e.event_time >= d.day - INTERVAL '29 days'
AND e.event_time < d.day + INTERVAL '1 day') AS mau_rolling
FROM days d
ORDER BY d.day;В Postgres COUNT(DISTINCT ...) OVER (...) не поддерживается (ошибка «DISTINCT is not implemented for window functions»), поэтому скользящий MAU считаем через коррелированный подзапрос.
Задача: Retention
«Для каждой когорты регистрации покажи retention на Day 1, 7, 30».
WITH cohorts AS (
SELECT
user_id,
DATE(signup_time) AS signup_date
FROM users
),
activity AS (
SELECT DISTINCT
user_id,
DATE(event_time) AS active_date
FROM events
)
SELECT
c.signup_date AS cohort,
COUNT(DISTINCT c.user_id) AS cohort_size,
COUNT(DISTINCT CASE
WHEN a.active_date = c.signup_date + INTERVAL '1 day' THEN c.user_id
END) * 1.0 / COUNT(DISTINCT c.user_id) AS day_1_retention,
COUNT(DISTINCT CASE
WHEN a.active_date = c.signup_date + INTERVAL '7 days' THEN c.user_id
END) * 1.0 / COUNT(DISTINCT c.user_id) AS day_7_retention
FROM cohorts c
LEFT JOIN activity a ON c.user_id = a.user_id
WHERE c.signup_date >= CURRENT_DATE - INTERVAL '60 days'
GROUP BY c.signup_date
ORDER BY c.signup_date;Задача: Подряд идущие события
«Найди пользователей с 3 днями подряд активности».
Трюк с оконной функцией:
WITH daily_activity AS (
SELECT DISTINCT user_id, DATE(event_time) AS active_day
FROM events
),
numbered AS (
SELECT
user_id,
active_day,
active_day - INTERVAL '1 day' * ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY active_day
) AS streak_group
FROM daily_activity
),
streaks AS (
SELECT
user_id,
streak_group,
COUNT(*) AS streak_length
FROM numbered
GROUP BY user_id, streak_group
)
SELECT DISTINCT user_id
FROM streaks
WHERE streak_length >= 3;Трюк: вычитание ROW_NUMBER из даты даёт одинаковое значение для подряд идущих дней.
Задача: Второй заказ
«Для каждого пользователя найди дату второго заказа».
SELECT
user_id,
order_date AS second_purchase
FROM (
SELECT
user_id,
order_date,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY order_date
) AS rn
FROM orders
) t
WHERE rn = 2;Вариации: «разница в днях между 1-й и 2-й покупкой» — использовать LAG():
SELECT
user_id,
order_date - LAG(order_date) OVER (
PARTITION BY user_id ORDER BY order_date
) AS days_between
FROM orders;Задача: Медиана
«Найди медианную зарплату по отделам».
PostgreSQL:
SELECT
department,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary
FROM employees
GROUP BY department;Без PERCENTILE — через оконные функции:
SELECT DISTINCT
department,
FIRST_VALUE(salary) OVER (
PARTITION BY department
ORDER BY CASE WHEN salary_rank_asc = salary_rank_desc THEN 0
WHEN salary_rank_asc - salary_rank_desc = 1 THEN 0
ELSE 1 END,
ABS(salary_rank_asc - salary_rank_desc)
) AS median
FROM (
SELECT department, salary,
RANK() OVER (PARTITION BY department ORDER BY salary) AS salary_rank_asc,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank_desc
FROM employees
) t;Сложно — тест на глубокое знание SQL. Редко встречается прямо на собесе.
Задача: N-е по величине значение
«Найди 3-ю по величине продажу в каждой категории».
SELECT category, sale_amount
FROM (
SELECT
category,
sale_amount,
DENSE_RANK() OVER (
PARTITION BY category
ORDER BY sale_amount DESC
) AS rnk
FROM sales
) t
WHERE rnk = 3;Задача: Gaps and islands (пропуски и участки)
«Найди пропущенные даты в последовательности».
WITH bounds AS (
SELECT
DATE,
LAG(DATE) OVER (ORDER BY DATE) AS prev_date
FROM events_calendar
)
SELECT
prev_date + INTERVAL '1 day' AS gap_start,
DATE - INTERVAL '1 day' AS gap_end
FROM bounds
WHERE DATE - prev_date > 1;Подготовка к SQL-интервью — главный приоритет для аналитика. В тренажёре Карьерник собрано 500+ SQL-задач с реальных собесов с разборами и объяснениями.
Популярные задачи по компаниям
Яндекс. Retention, когортный анализ, оконные функции, арифметика дат.
Тинькофф. Финансовые агрегации, временные ряды, анализ транзакций.
Avito. Объявления, поведение пользователей, анализ сессий, воронки.
Ozon. Заказы, склад, рекомендации, воронки конверсии.
Сбер. Платежи, антифрод, финансовые риски.
Альфа-Банк. Аналитика транзакций, сегментация клиентов.
Каждая компания имеет свои любимые типы задач — учитывайте это в подготовке.
Советы по live coding
1. Думать вслух. «Я бы начал с... потом join... потом агрегирую... фильтрую».
2. Обсудить подход перед кодом. Понять, правильное ли направление.
3. Начать с маленького примера. «Допустим, у нас 3 пользователя, каждый с 2 заказами». Проверить запрос вручную.
4. Проверить граничные случаи. NULL, дубликаты, нулевые значения, пустые результаты.
5. Читать свой код. «Здесь я группирую по..., фильтрую...». Показывает, что понимаете, что написали.
6. Спрашивать про форматы. «Даты в строке или в типе date?» «Есть дубликаты строк?» Хороший знак.
Типичные ошибки на собесе
Писать сразу финальный запрос. Без обсуждения подхода. Рискованно.
Синтаксические ошибки от паники. Печатать невнимательно. Проверяйте решение перед отправкой.
Не использовать CTE. Сложные вложенные подзапросы — труднее читать и дебажить. CTE предпочтительнее.
Забыть про NULL. COUNT(*) vs COUNT(col) vs COUNT(DISTINCT col) — разница есть.
Игнорировать производительность. Для джуновских собесов не важно, но на senior-уровне могут спросить «какая будет сложность?» или «как оптимизировать?».
Замереть в тишине. Если не знаете ответ — проговаривайте ход мыслей. Иногда интервьюер даёт подсказки.
Как готовиться
Решать задачи. LeetCode SQL, StrataScratch, DataLemur. Начните с medium, потом hard.
Писать на бумаге. Не только на компьютере. Некоторые собесы без автодополнения.
Повторить SQL-фичи. Оконные функции, regex, JSON-операторы, CTE — часто пропускают на начальном уровне.
Mock-интервью. С друзьями или коучами. Опыт реального давления.
Читать реальные вопросы с собесов. Glassdoor, тематические блоги — хороший источник частых паттернов.
Читайте также
FAQ
Какой SQL-диалект учить?
PostgreSQL — универсальная база. В компании могут быть MySQL, ClickHouse, BigQuery — синтаксис немного отличается.
Нужны ли сертификаты по SQL?
Не решающие. Реальные проекты, GitHub, примеры решённых задач в LinkedIn — ценнее.
Запоминать все оконные функции?
Основные — ROW_NUMBER, RANK, LAG, LEAD, SUM OVER. Остальные — справочно.
Если не знаю синтаксис?
Посмотреть в справочнике — нормально в take-home. В live — вспомните логически, объясните, что хотите сделать, интервьюер подскажет синтаксис.