SQL на собеседовании аналитика: что спрашивают

Проверь себя · 1/3разбор после ответа
В отчёте нужно вывести всех пользователей и количество их заказов, включая тех, у кого заказов нет. Какой тип соединения между 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;
Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Задача: Медиана

«Найди медианную зарплату по отделам».

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 — вспомните логически, объясните, что хотите сделать, интервьюер подскажет синтаксис.