SQL задачи на собеседовании: типы, логика решения и типичные ошибки
На собеседовании по SQL проверяют не память, а умение разложить вопрос на шаги и выбрать подходящий инструмент.
- Разминка: SELECT, WHERE, ORDER BY, LIMIT
- Агрегация и группировка: где чаще всего спотыкаются
- Соединения: INNER, LEFT и тихая потеря строк
- Подзапросы и CTE: разбить задачу на шаги
- Оконные функции: ранжирование, итоги, сравнение периодов
- Даты, когорты и воронки: метрики, которые спрашивают чаще всего
- Оптимизация: план, индексы и лишние строки
- Как вести себя на собеседовании, пока решаете задачу
- Типичные ошибки, которые мешают дойти до оффера
Разминка: SELECT, WHERE, ORDER BY, LIMIT
Разминка показывает, свободно ли вы пишете базовый запрос. Порядок работы такой: FROM и WHERE отбирают строки, GROUP BY с HAVING собирают группы, SELECT считает выражения, и лишь в конце работают ORDER BY и LIMIT.
Поэтому сортировка идёт до LIMIT: LIMIT обрезает уже упорядоченный результат. Без ORDER BY порядок строк не гарантирован, так что проговорите допущение: «топ считаю по убыванию выручки, равенства развожу по id».
- задача: топ-10 клиентов по выручке за месяц. Логика: SUM(amount) по client_id, затем ORDER BY revenue DESC LIMIT 10.
- задача: десять самых дорогих сделок. Логика: только ORDER BY amount DESC LIMIT 10, группировка здесь схлопнет строки.
- задача: последние 20 событий. Логика: ORDER BY created_at DESC LIMIT 20, при повторах дат добавьте id.
Агрегация и группировка: где чаще всего спотыкаются
COUNT(*) считает строки, а COUNT(колонка) — только те, где значение не NULL. Разница видна после LEFT JOIN: COUNT(o.id) покажет ноль заказов у клиента, а COUNT(*) вернёт единицу, ведь строка клиента осталась.
SUM и AVG пропускают NULL, поэтому AVG(x) делит сумму на число непустых значений — частая причина неверного среднего чека. WHERE фильтрует строки до группировки, HAVING — уже собранные группы, и перепутать их легко.
- задача: клиенты с двумя и более заказами. Логика: GROUP BY client_id HAVING COUNT(*) > 1.
- задача: дубли по email. Логика: GROUP BY email HAVING COUNT(*) > 1.
- задача: средний чек. Логика: SUM(amount) разделить на COUNT(DISTINCT order_id), а не усреднять строки позиций.
Соединения: INNER, LEFT и тихая потеря строк
INNER JOIN оставляет только совпадения, LEFT JOIN сохраняет все строки левой таблицы и подставляет NULL справа. Если справа несколько совпадений, LEFT JOIN размножит строки и завысит суммы — самая дорогая ошибка в задачах на метрики.
Условие на правую таблицу в WHERE превращает LEFT JOIN в INNER: чтобы сохранить строки без пары, пишите условие в ON. Соединение по нескольким ключам нужно там, где уникальна только их пара.
- задача: клиенты без заказов. Логика: LEFT JOIN orders ON ... WHERE orders.id IS NULL.
- задача: сумма выросла после соединения с позициями. Логика: агрегируйте позиции в подзапросе, затем соединяйте один к одному.
- задача: появилось декартово произведение. Логика: проверьте условие соединения и уникальность ключа.
Подзапросы и CTE: разбить задачу на шаги
Подзапрос читаемее соединения, когда на строку нужна одна метрика или фильтр по агрегату: WHERE amount > (SELECT AVG(amount) FROM payments). Коррелированный подзапрос выполняется для каждой строки внешнего запроса и часто проигрывает соединению или оконной функции.
WITH позволяет назвать промежуточные шаги: сначала активность по дням, потом сглаживание, потом метрика. Так проще и писать, и объяснять ход решения.
- задача: клиенты с выручкой выше средней. Логика: CTE с суммой по клиенту, затем фильтр по среднему из этого же CTE.
- задача: воронка из трёх шагов. Логика: три CTE, по одному на шаг.
- задача: убрать коррелированный подзапрос. Логика: замените его соединением с агрегатом.
Оконные функции: ранжирование, итоги, сравнение периодов
ROW_NUMBER даёт уникальные номера, RANK повторяет номер при равенстве и пропускает следующие, DENSE_RANK повторяет без пропусков. «По одному заказу на клиента» — это ROW_NUMBER, «все, кто попал в топ-3» — DENSE_RANK.
PARTITION BY делит строки на группы, ORDER BY задаёт порядок внутри группы. Окно считается после WHERE и GROUP BY, поэтому фильтровать по его результату прямо в WHERE нельзя — нужен отдельный шаг в CTE.
- задача: последний заказ каждого клиента. Логика: ROW_NUMBER() OVER (PARTITION BY client_id ORDER BY created_at DESC) = 1.
- задача: накопительный итог выручки. Логика: SUM(amount) OVER (PARTITION BY client_id ORDER BY paid_at).
- задача: сравнить метрику с прошлым днём. Логика: LAG(metric) OVER (ORDER BY dt), для следующего значения — LEAD.
- задача: топ-3 клиента в каждом регионе. Логика: RANK() OVER (PARTITION BY region ORDER BY revenue DESC) <= 3.
Даты, когорты и воронки: метрики, которые спрашивают чаще всего
Активность по дням считается через усечение даты до дня и COUNT(DISTINCT user_id), при этом помните про часовой пояс: граница дня зависит от зоны расчёта.
Удержание строится от когорты — даты первого действия, дальше считается номер дня жизни и собирается матрица «когорта × день». В воронке важен один и тот же знаменатель на всех шагах, а пропущенные даты лечатся соединением с календарём.
- задача: DAU по дням. Логика: GROUP BY усечённая дата, COUNT(DISTINCT user_id).
- задача: удержание на 1, 7 и 30 день. Логика: когорта по MIN(дата), затем разница в днях.
- задача: воронка регистрация → первый заказ → повторный заказ. Логика: уникальные пользователи на каждом шаге.
- задача: заполнить пропущенные дни нулями. Логика: LEFT JOIN со справочником дат.
Оптимизация: план, индексы и лишние строки
Начните с плана: EXPLAIN показывает, как база собирается читать таблицы, EXPLAIN ANALYZE добавляет факт выполнения. Сравните оценку строк с реальной — большое расхождение говорит об устаревшей статистике.
Индекс работает, когда условие применяется к колонке напрямую. Запись date_trunc('day', created_at) = ... или created_at::date = ... обычно лишает индекс смысла: перепишите её в диапазон created_at >= '2025-01-01' AND created_at < '2025-01-02'.
- задача: запрос читает всю таблицу. Логика: ищите функцию вокруг колонки в WHERE.
- задача: соединение раздуло выборку. Логика: сначала агрегат, потом соединение.
- задача: когда честно нужна временная таблица. Логика: многошаговая агрегация, результат нужен дважды.
Как вести себя на собеседовании, пока решаете задачу
Уточняющие вопросы экономят половину времени: какие таблицы и ключи, что считается дублем, нужен уникальный пользователь или все события, какой период брать, как трактовать NULL.
Проговаривайте допущения: «оплатой считаю только status = 'paid'», «один user_id — один человек», и проверяйте логику руками на трёх-четырёх строках. Если решение не идёт, назовите, что именно неизвестно, предложите вариант «в лоб» и улучшайте его: молчание хуже простого ответа.
- задача: в условии нет периода. Логика: сначала спросите, потом пишите запрос.
- задача: в колонке есть NULL. Логика: уточните, включать такие строки в метрику или нет.
- задача: решение не сходится. Логика: сформулируйте гипотезу вслух и проверьте её на нескольких строках.
Типичные ошибки, которые мешают дойти до оффера
NULL в сравнениях: col = NULL никогда не истинно, нужны IS NULL и IS NOT NULL, а NOT IN с NULL внутри подзапроса вернёт пустой результат. Неявное приведение типов даёт странные совпадения и убивает индекс.
Дальше по списку: COUNT по неверной колонке, потеря строк в соединении и неверный знаменатель метрики. Последнее встречается чаще всего: средний чек считают по строкам позиций, хотя делить нужно на число заказов.
- задача: NOT IN вернул пусто. Логика: уберите NULL из подзапроса или замените на NOT EXISTS.
- задача: метрика не совпала с ожиданием. Логика: посчитайте числитель и знаменатель отдельно.
Что сделать: короткий чеклист
- Проговорите порядок выполнения: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.
- Перед LIMIT всегда ставьте ORDER BY и добавляйте tie-breaker по id или дате.
- Сравнивайте COUNT(*) до и после соединения — так видны потерянные и размноженные строки.
- Для метрик считайте числитель и знаменатель отдельными запросами.
- Фильтр по оконной функции выносите в отдельный шаг через CTE.
- Проверяйте логику на нескольких строках, где ответ известен заранее.
- Смотрите EXPLAIN перед тем, как назвать запрос готовым.
- Проговаривайте допущения вслух до того, как начнёте писать код.
Где тренировать
Темы из гайда разобраны в тестах портала — каждый вопрос с объяснением ответа:
- Задачи и вопросы по SQL в тренажёре
- Вводная тема курса SQL — бесплатно и без регистрации
- Вопросы на собеседовании аналитика данных
- Экзамен уровня Мидл: 20 вопросов — Тесты для уровня Мидл
- Подписка: 590 ₽ в месяц, 3 990 ₽ в год, 7 490 ₽ навсегда
Возьмите одну тему и решите пять задач подряд: разминку на SELECT, затем агрегацию, соединения и оконные функции. На Proanalytics.tech курс SQL собран из 159 вопросов и 13 тем, вводная тема открыта бесплатно и без регистрации, а диагностика из 20 вопросов покажет, с чего начать. Дальше — практика: каждый разобранный запрос делает ваш ответ на собеседовании спокойнее.
Диагностика из 20 вопросов покажет ваш уровень и темы, которые стоит подтянуть. Вводная тема любого курса открыта бесплатно и без регистрации.
Создать аккаунт — вводная тема бесплатна Пройти демо-тест без регистрации