Вопросы по SQL на собеседовании: 20 задач с разбором решений
Задачи, которые встречаются чаще всего, — с решениями и объяснением, почему интервьюер спрашивает именно это.
SQL-секция обманчива: синтаксис учится за неделю, а срезаются на ней регулярно. Причина в том, что спрашивают не синтаксис, а понимание того, что запрос делает с данными.
С чего всё начинается: порядок выполнения
Вопрос выглядит теоретическим, но из него следуют ответы на половину остальных.
Запрос пишут начиная с SELECT, а выполняется он начиная с FROM. Отсюда три следствия, которые и проверяют:
- Алиас из
SELECTнельзя использовать вWHERE— на тот момент его ещё нет. ВORDER BYможно: он выполняется позже. WHEREфильтрует строки до группировки,HAVING— группы после. Условие по агрегату вWHEREне работает.WHEREдешевлеHAVING: отсекать нужно как можно раньше.
Джойны
Чем LEFT JOIN отличается от INNER JOIN?
INNER оставляет только совпавшие строки, LEFT — все строки левой таблицы, подставляя NULL там, где справа ничего не нашлось.
Найдите пользователей без единого заказа
Задача на LEFT JOIN с проверкой на NULL:
SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
Здесь же любят спросить про альтернативы: NOT EXISTS обычно читается лучше и на больших таблицах чаще выигрывает, а NOT IN — ловушка, потому что при NULL в подзапросе он вернёт пустой результат.
SELECT id, email FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
Почему после джойна строк стало больше?
Частый вопрос с подвохом. Если справа на одну строку слева приходится несколько совпадений, строки размножатся. Классический случай — джойн заказов с позициями заказов, после которого сумма по заказам считается неверно.
Правильная реакция: сначала агрегировать, потом джойнить.
SELECT o.id, o.created_at, i.total
FROM orders o
JOIN (
SELECT order_id, SUM(price * qty) AS total
FROM order_items GROUP BY order_id
) i ON i.order_id = o.id;
Группировки и агрегаты
Чем COUNT(*) отличается от COUNT(column)?
COUNT(*) считает строки, COUNT(column) — строки, где столбец не NULL. COUNT(DISTINCT column) — уникальные непустые значения. На этом строится масса ошибок в отчётности.
Как ведут себя агрегаты с NULL?
SUM, AVG, MAX пропускают NULL. Отсюда неприятность: AVG по столбцу с пропусками считает среднее по заполненным, а не по всем строкам — если нужно иначе, COALESCE(col, 0).
Топ-3 товара по выручке в каждой категории
Задача на оконные функции — самая частая на позициях аналитика.
SELECT category, product, revenue
FROM (
SELECT category, product, SUM(price * qty) AS revenue,
ROW_NUMBER() OVER (
PARTITION BY category ORDER BY SUM(price * qty) DESC
) AS rn
FROM sales
GROUP BY category, product
) t
WHERE rn <= 3;
Оконную функцию нельзя фильтровать в WHERE того же уровня — она вычисляется после. Отсюда подзапрос или CTE.
Оконные функции
Чем ROW_NUMBER отличается от RANK и DENSE_RANK?
ROW_NUMBER— сквозная нумерация, при равных значениях порядок произвольный: 1, 2, 3, 4.RANK— одинаковым значениям одинаковый ранг, следующий с пропуском: 1, 2, 2, 4.DENSE_RANK— то же, но без пропуска: 1, 2, 2, 3.
Уточняющий вопрос обычно такой: какую взять для «топ-3 с учётом ничьих»? Ответ — DENSE_RANK, иначе при двух вторых местах третье потеряется.
Накопительный итог по дням
SELECT day, revenue,
SUM(revenue) OVER (ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running
FROM daily;
Указывать рамку явно стоит: по умолчанию при наличии ORDER BY используется RANGE, и на одинаковых датах результат будет не тем, которого ждут.
Насколько выросли продажи по сравнению с прошлым месяцем
SELECT month, revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS diff
FROM monthly;
LAG берёт предыдущую строку, LEAD — следующую. Оба принимают смещение и значение по умолчанию: LAG(revenue, 12, 0) — год назад.
Практические задачи
Удалите дубликаты, оставив по одной строке
DELETE FROM t
WHERE id NOT IN (
SELECT MIN(id) FROM t GROUP BY email
);
Вариант через оконную функцию читается лучше и обычно быстрее:
DELETE FROM t
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY email ORDER BY created_at
) AS rn FROM t
) x WHERE rn > 1
);
Найдите вторую по величине зарплату
Задача-классика. Наивное ORDER BY salary DESC LIMIT 1 OFFSET 1 ломается при дубликатах — вернёт ту же максимальную.
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
Если спросят про N-ю — DENSE_RANK.
Посчитайте удержание по когортам
Тяжёлая задача, которую дают аналитикам. Смысл: сгруппировать пользователей по месяцу первого визита и посмотреть, сколько вернулось через месяц, два, три.
WITH first_seen AS (
SELECT user_id, DATE_TRUNC('month', MIN(event_at)) AS cohort
FROM events GROUP BY user_id
)
SELECT f.cohort,
DATE_TRUNC('month', e.event_at) AS period,
COUNT(DISTINCT e.user_id) AS users
FROM events e
JOIN first_seen f ON f.user_id = e.user_id
GROUP BY 1, 2
ORDER BY 1, 2;
Здесь оценивают не столько запрос, сколько понимание, что такое когорта и зачем она нужна.
Производительность
Почему индекс не помогает?
Частые причины, и хорошо назвать хотя бы три:
- Функция на столбце:
WHERE DATE(created_at) = '2026-01-01'индекс не использует. Переписывать через диапазон. LIKE '%текст'с ведущим процентом — обычный B-tree бесполезен.- Низкая селективность: если условие отбирает половину таблицы, планировщик предпочтёт полный проход, и будет прав.
- Порядок столбцов в составном индексе: индекс по
(a, b)работает для условия поa, но не по одномуb.
Как понять, что запрос медленный из-за плана?
EXPLAIN ANALYZE. Достаточно уметь читать три вещи: используется ли индекс или идёт seq scan, сколько строк планировщик ожидал против фактических (сильное расхождение — устаревшая статистика), и где в дереве накапливается время.
Ответ «добавил бы индекс» без вопроса о том, какие запросы к таблице идут, — слабый. Индексы ускоряют чтение и замедляют запись, и на таблице с интенсивной вставкой это реальный размен.
Что делать, если задача не решается
Пишите по шагам и вслух. Соберите сначала промежуточный набор данных, посмотрите на него, потом навешивайте агрегацию. CTE для этого удобнее вложенных подзапросов — и читается интервьюером легче.
Если синтаксис вылетел из головы, скажите словами, что хотите сделать: «здесь нужна оконная функция с разбиением по категории». Идея важнее точного написания — её и оценивают.
Коротко
- Порядок выполнения объясняет половину остальных вопросов.
NOT INсNULLи размножение строк после джойна — две самые частые ловушки.ROW_NUMBER,RANK,DENSE_RANKнужно уметь различать вслух.- При вопросах о скорости начинайте с
EXPLAIN, а не с «добавлю индекс». - Не решается — стройте запрос шагами через CTE и проговаривайте.
Шпора — ИИ-помощник, который слышит вопрос интервьюера и за секунду выводит опору для ответа. Работает на вашем компьютере с любой видеосвязью.
Попробовать бесплатно