Какой уровень SQL нужен, чтобы пройти собес на продуктового аналитика

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

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

Сколько SQL хватает

Первый — то, без чего разговор даже не начинается. SELECT с фильтрами, JOIN и понимание разницы между LEFT и INNER, GROUP BY с агрегатами, HAVING, сортировки. Спотыкаешься здесь — секция закончилась.

Второй вытягивает середину и закрывает офферы уровня Junior–Middle. Оконные функции (ROW_NUMBER, RANK, LAG и LEAD, нарастающий итог), CTE, DATE_TRUNC с интервалами, условная агрегация через CASE. Именно тут живут продуктовые задачи: когорты, retention, воронки.

Третий — плюс в карму. Дедупликация, чтение плана запроса, ловушки NOT IN с NULL.

Что должно отскакивать от зубов

Прогони себя по списку честно. Не «вспомнить синтаксис», а писать сразу, без гугла:

JOIN нескольких таблиц — и чтобы ты замечал, когда строки начинают дублироваться. GROUP BY с HAVING и внятная разница между WHERE и HAVING. ROW_NUMBER для дедупликации и «первого/второго по порядку». LAG для сравнения с прошлым периодом. CTE, чтобы резать сложный запрос на читаемые куски. DATE_TRUNC для помесячной агрегации. CASE внутри SUM для воронок. И NULL — IS NULL, COALESCE и почему NOT IN иногда ничего не возвращает.

Четыре задачи с собеса и как их решать

Схема простая, держи в голове: orders (id, user_id, amount, status, paid_at), order_items (order_id, product_id, price, qty), products (id, category_id), events (user_id, event_date). Такие же задачи, только с автопроверкой в браузере, лежат в SQL-тренажёре Заскуль Питона (https://zasqlpython.ru/sql-sandbox) — первые открыты без регистрации.

Задача 1. Категории с выручкой выше порога

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

SELECT c.name AS category, SUM(oi.price * oi.qty) AS revenue FROM order_items oi JOIN orders o ON o.id = oi.order_id JOIN products p ON p.id = oi.product_id JOIN categories c ON c.id = p.category_id WHERE o.status = 'paid' GROUP BY c.name HAVING SUM(oi.price * oi.qty) > 1000000 ORDER BY revenue DESC;

Вся суть — куда что положить. Порог на сумму идёт в HAVING, потому что фильтруем уже посчитанные группы. А status = 'paid' — в WHERE: это про строки, до группировки. Перепутаешь местами — не соберётся.

Задача 2. Второй заказ каждого пользователя

Формулировка: для каждого пользователя найди его второй по времени заказ. Любимая проверка на оконки — и на то, понимаешь ли ты, почему её нельзя фильтровать в лоб.

SELECT user_id, paid_at, amount FROM ( SELECT user_id, paid_at, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY paid_at) AS rn FROM orders) t WHERE rn = 2;

ROW_NUMBER нумерует заказы внутри каждого пользователя по дате. Обернуть в подзапрос обязательно: оконная функция считается уже после WHERE, так что фильтр rn = 2 живёт только на внешнем уровне. Вот на этом и подлавливают — половина кандидатов пишет WHERE ROW_NUMBER()... = 2 и удивляется ошибке.

Задача 3. Месячный retention когорты

Формулировка: посчитай, какая доля пользователей, впервые пришедших в месяце, вернулась в следующем. Самая продуктовая задача секции — тут сразу и когорты, и CTE, и даты.

WITH first_month AS ( SELECT user_id, DATE_TRUNC('month', MIN(event_date)) AS cohort FROM events GROUP BY user_id ), activity AS ( SELECT DISTINCT user_id, DATE_TRUNC('month', event_date) AS active_month FROM events ) SELECT f.cohort, COUNT(DISTINCT f.user_id) AS cohort_size, COUNT(DISTINCT CASE WHEN a.active_month = f.cohort + INTERVAL '1 month' THEN a.user_id END) AS retained, ROUND(100.0 * COUNT(DISTINCT CASE WHEN a.active_month = f.cohort + INTERVAL '1 month' THEN a.user_id END) / COUNT(DISTINCT f.user_id), 1) AS retention_pct FROM first_month f LEFT JOIN activity a ON a.user_id = f.user_id GROUP BY f.cohort ORDER BY f.cohort;

Читается по шагам. Первый CTE — месяц первого визита каждого (это и есть когорта). Второй — все месяцы, когда человек был активен. Дальше считаем размер когорты и сколько из неё вернулось ровно через месяц. Напишешь это без запинки — второй уровень у тебя закрыт.

Задача 4. Рост выручки месяц к месяцу

Формулировка: выручка по месяцам и её прирост в процентах к предыдущему. Проверяют LAG и аккуратность с делением.

WITH monthly AS ( SELECT DATE_TRUNC('month', paid_at) AS month, SUM(amount) AS revenue FROM orders WHERE status = 'paid' GROUP BY 1 ) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_revenue, ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 1) AS mom_growth_pct FROM monthly ORDER BY month;

LAG подтягивает выручку прошлого месяца в ту же строку — дальше обычная арифметика. И вот тут интервьюер почти всегда спрашивает про край: первый месяц даст NULL, а если в прошлом месяце была нулевая выручка — деление на ноль. Проговоришь это сам, до вопроса, — считай, балл в кармане.

На чём сыпятся чаще всего

Из раза в раз одно и то же. Путают WHERE и HAVING — тянут агрегат в WHERE. Фильтруют оконку в том же запросе, забыв про подзапрос. Не замечают, как джойн с дублями раздувает SUM. Ловят пустой результат на NOT IN, где в списке затесался NULL. И самое частое — молчат. Пишут в тишине, будто на экзамене. А интервьюер оценивает не идеальный синтаксис, а то, как ты рассуждаешь. Думай вслух — это буквально половина оценки.

Как это натренировать

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

Если начинаешь с нуля — бери бесплатный курс «SQL с нуля» (https://zasqlpython.ru/course): десять частей от SELECT до оконных функций, всё решается прямо в браузере, базу ставить не надо.

Когда база есть — набивай руку на задачах с автопроверкой. В тренажёре (https://zasqlpython.ru/sql-sandbox) больше пятисот задач с реальных собеседований, движок PostgreSQL прямо в браузере, решение проверяется само.

Перед самим собесом прогони разборы вопросов с собеседований (https://zasqlpython.ru/interviews).

Если честно, четыре задачи выше — это и есть репрезентативный срез. Решаешь их без подсказок минут за пятнадцать — иди на собес спокойно, SQL тебя там не удивит.

11