SQL на собеседовании: 10 задач с решениями и что за ними проверяют
SQL на собеседовании проверяют не на знание синтаксиса. Синтаксис гуглится за секунду. Проверяют, как вы думаете про данные: понимаете ли, что делает LEFT JOIN с фильтром, отличаете ли WHERE от HAVING, что произойдёт со строкой, где притаился NULL.
Ниже 10 задач, которые я слышал на собесах на аналитика и дата-инженера чаще всего. К каждой решение и короткий разбор: что за вопросом на самом деле. Диалект PostgreSQL, но идеи переносятся почти куда угодно.
1. LEFT JOIN, который тайно стал INNER
Классика, на которой валятся даже мидлы. Есть таблицы users и orders. Нужны все пользователи и их заказы за январь, включая тех, кто в январе не покупал.
Первый порыв обычно такой:
SELECT u.id, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.created_at >= '2026-01-01';
И это неверно. Условие на o.created_at в WHERE выкидывает строки, где заказа нет: там created_at равен NULL, а NULL >= '2026-01-01' не истина. LEFT JOIN превратился в INNER. Пользователи без заказов пропали.
Правильно: условие на правую таблицу переносим в ON.
SELECT u.id, o.amount
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
AND o.created_at >= '2026-01-01';
Что проверяют: понимаете ли вы порядок, в котором база собирает результат. JOIN сначала, WHERE потом. Фильтр по правой таблице в WHERE почти всегда ломает LEFT JOIN.
2. Вторая по величине зарплата
Вопрос-ветеран. Найти второй по величине оклад в таблице employees(salary).
Через подзапрос:
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
Через оконку, если нужен не только второй, а произвольный N-й:
SELECT DISTINCT salary
FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) t
WHERE rnk = 2;
DENSE_RANK, а не RANK: если два человека получают одинаковый максимум, RANK пропустит второе место, и вы вернёте третью зарплату вместо второй. Тонкость, ради которой вопрос и задают.
Что проверяют: знаете ли вы больше одного способа и понимаете ли разницу между ними. Скажете «а ещё бывает OFFSET 1 LIMIT 1, но он споткнётся о дубликаты максимума» - это уже сильный ответ.
3. WHERE или HAVING
Дают запрос с агрегацией и спрашивают, куда воткнуть условие. Нужны отделы, где средняя зарплата выше 100 000.
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 100000;
Условие на результат агрегата идёт в HAVING. В WHERE его поставить нельзя: WHERE отрабатывает до группировки, когда AVG ещё не посчитан. А вот фильтр по отдельным строкам (например, только активные сотрудники) наоборот идёт в WHERE, до GROUP BY, чтобы не тащить лишнее в агрегат.
Что проверяют: держите ли вы в голове конвейер выполнения: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Это тот же корень, что и в задаче про LEFT JOIN.
4. ROW_NUMBER, RANK, DENSE_RANK
Просят объяснить разницу. Проще всего на одном примере. Зарплаты 100, 100, 90:
ROW_NUMBERдаст 1, 2, 3. Просто нумерация, дубликаты ей безразличны.RANKдаст 1, 1, 3. Одинаковым значениям один ранг, следующий сдвинут на число дублей.DENSE_RANKдаст 1, 1, 2. Тоже общий ранг, но без дыр.
SELECT
name,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,
RANK() OVER (ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY salary DESC) AS drnk
FROM employees;
Что проверяют: был ли у вас реальный опыт с оконными функциями. Кто их только читал, обычно путает RANK и DENSE_RANK. Кто применял для «топ-3 в каждой категории», отвечает не задумываясь.
5. Топ-N в каждой группе
Прямое продолжение прошлой. Нужны три самых дорогих товара в каждой категории.
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;
Ключевое слово PARTITION BY: оно перезапускает нумерацию внутри каждой категории. Без него ROW_NUMBER пронумерует всю таблицу насквозь, и вы получите три самых дорогих товара вообще, а не по категориям.
Что проверяют: понимаете ли вы, что оконка считается в рамках окна, а не всей таблицы. Это тот самый навык, который отделяет «читал про window functions» от «делал отчёты».
6. Накопительный итог
Посчитать нарастающий итог выручки по дням.
SELECT
day,
revenue,
SUM(revenue) OVER (ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM daily_revenue;
SUM с OVER (ORDER BY ...) превращается из агрегата в оконную функцию и накапливает сумму по мере движения по строкам. Рамку ROWS BETWEEN ... можно опустить, поведение по умолчанию такое же, но проговорить её на собесе полезно: покажете, что понимаете, что под капотом.
Что проверяют: умеете ли вы решать «нарастающее» без самосоединения таблицы саму на себя. Старый способ через JOIN работает, но на больших данных он медленный, и грамотный ответ начинается с оконки.
7. Ловушка с NULL
Любимый вопрос на внимательность. Чем COUNT(*) отличается от COUNT(column)?
COUNT(*) считает строки. COUNT(column) считает строки, где column не NULL. На таблице с пропусками они дадут разные числа, и это регулярно ломает отчёты.
Второй заход той же темы: почему NOT IN иногда возвращает пустоту?
SELECT * FROM a
WHERE id NOT IN (SELECT id FROM b);
Если в b есть хоть один NULL, весь NOT IN схлопывается в неопределённость и результат пуст. NULL в SQL это не значение, а «неизвестно», и сравнение с ним не истина и не ложь. Безопаснее NOT EXISTS:
SELECT * FROM a
WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.id = a.id);
Что проверяют: обжигались ли вы на NULL в бою. Тот, кто обжигался, помнит про NOT IN навсегда.
8. Убрать дубликаты, оставив один
Есть таблица с задвоенными записями. Нужно оставить по одной на каждый email, самую свежую.
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
FROM users
) t
WHERE rn > 1
);
Нумеруем дубли внутри каждого email, самой свежей достаётся rn = 1, всё остальное под нож. Тот же ROW_NUMBER из задачи про топ-N, только теперь он служит дедупликации.
Что проверяют: видите ли вы оконные функции как универсальный инструмент, а не как трюк для отчётов. Хороший знак, если спросите: а нам точно удалять, или сначала отобрать дубли и показать заказчику.
9. Почему запрос тормозит
Полу-теоретический вопрос. Показывают медленный запрос и просят предположить причину. Частый виновник: функция, навешенная на колонку в WHERE.
-- индекс по created_at здесь не сработает
WHERE DATE(created_at) = '2026-01-01'
-- а здесь сработает
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'
Как только колонка завёрнута в функцию, база не может воспользоваться обычным индексом по ней и уходит в полное сканирование. Лечится переписыванием условия на диапазон. Дальше можно упомянуть EXPLAIN ANALYZE, чтобы увидеть план и понять, где именно теряется время.
Что проверяют: думаете ли вы про то, как запрос исполняется, а не только про то, что он возвращает. На позициях с большими данными это половина работы.
10. CTE против подзапроса
Просят переписать вложенный подзапрос и объяснить, зачем. WITH (CTE) не столько про скорость, сколько про читаемость: многоэтажный запрос разбивается на именованные шаги, которые читаются сверху вниз.
WITH active AS (
SELECT * FROM users WHERE last_login > NOW() - INTERVAL '30 days'
),
spend AS (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
)
SELECT a.id, s.total
FROM active a
JOIN spend s ON s.user_id = a.id;
А если добавить RECURSIVE, тем же CTE можно обойти иерархию: дерево подчинения, категории с подкатегориями, цепочку связей. Это уже вопрос на синьора, но знать, что рекурсивный CTE существует, стоит.
Что проверяют: заботитесь ли вы о том, чтобы ваш SQL мог прочитать коллега. Читаемый запрос это не эстетство, это про то, кто будет разбираться в нём через полгода.
Как к этому готовиться
Прочитать разбор мало. На собесе рука должна писать PARTITION BY без раздумий, а глаз ловить NULL и фильтр на правой таблице сам. Это ставится только практикой: берёте задачу, пишете запрос руками, проверяете, ошибаетесь, правите.
Ровно под это в Сеньорчике собран трек SQL: задачи с вариантами и разбором, движок возвращает темы, где вы плывёте, и гоняет их, пока не отскакивает. Формат вопрос-разбор ближе к собесу, чем чтение статей. Начать можно бесплатно.
А эти десять держите под рукой перед собесом. Они закрывают процентов восемьдесят того, что реально спрашивают у аналитика по SQL.