Вопросы на собеседовании по SQL
Что спрашивают по SQL у стажёров, джунов и мидлов: JOIN и группировка, NULL, подзапросы и оконные функции, индексы и транзакции. В конце — четыре типовые задачи с короткими решениями.
JOIN, группировка и основы
-
Какие бывают JOIN?
INNER— только строки, у которых есть пара в обеих таблицах;LEFT— все строки левой таблицы и совпавшие из правой, остальное NULL;RIGHT— наоборот;FULL— все строки обеих;CROSS— все комбинации. Self join — соединение таблицы с самой собой, например сотрудника с его руководителем. -
При LEFT JOIN есть разница, писать условие в ON или в WHERE?
Есть. Условие в
ONучаствует в соединении: строки левой таблицы сохраняются, просто справа будет NULL. То же условие на правую таблицу вWHEREотбросит строки с NULL — и LEFT JOIN фактически превратится в INNER. -
Как найти клиентов без заказов?
LEFT JOIN orders o ON o.client_id = c.id WHERE o.id IS NULLилиWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.client_id = c.id). Вариант сNOT INопасен: если подзапрос вернёт хотя бы один NULL, результат будет пустым. -
В каком порядке выполняется SELECT?
Логически:
FROMиJOIN→WHERE→GROUP BY→HAVING→SELECT, где считаются и оконные функции →DISTINCT→ORDER BY→LIMIT. Поэтому псевдоним из SELECT нельзя использовать в WHERE, а в ORDER BY можно, и оконную функцию нельзя отфильтровать в WHERE того же запроса. -
Чем WHERE отличается от HAVING?
WHEREфильтрует строки до группировки,HAVING— группы после неё, поэтому в нём можно использовать агрегаты:HAVING COUNT(*) > 5. Условие без агрегатов лучше писать в WHERE — группировать придётся меньше строк. -
COUNT(*), COUNT(col) и COUNT(DISTINCT col) — в чём разница?
COUNT(*)считает все строки,COUNT(col)— строки, где col не NULL,COUNT(DISTINCT col)— число разных не-NULL значений. После LEFT JOIN считайте по колонке правой таблицы:COUNT(o.id)даст клиентам без заказов 0, аCOUNT(*)— 1. -
Чем UNION отличается от UNION ALL?
UNIONубирает дубликаты, и для этого базе приходится сортировать или хешировать результат.UNION ALLпросто склеивает наборы и работает быстрее. Если дублей быть не может или они нужны, берите UNION ALL. -
Чем DELETE отличается от TRUNCATE и DROP?
DELETEудаляет строки, можно с условием, при этом срабатывают триггеры на удаление.TRUNCATEочищает таблицу целиком и работает намного быстрее.DROPудаляет саму таблицу вместе со структурой. Откатывается ли TRUNCATE в транзакции, зависит от СУБД: в PostgreSQL — да, в MySQL и Oracle — нет.
NULL
-
Как NULL ведёт себя в сравнениях?
NULL означает «неизвестно», и любое сравнение с ним, даже
NULL = NULL, даёт не TRUE, а UNKNOWN. WHERE пропускает только TRUE, поэтомуcol = NULLне находит ничего — нужноIS NULL. По той же причинеcol <> 'x'не вернёт строки, где col равен NULL. -
Как агрегатные функции обрабатывают NULL?
SUM,AVG,MIN,MAXиCOUNT(col)пропускают NULL.AVGделит на число не-NULL значений, а не на число строк.SUMпо пустому набору возвращает NULL, а не 0, поэтому пишутCOALESCE(SUM(amount), 0). -
Как NULL ведёт себя в GROUP BY, DISTINCT и ORDER BY?
В GROUP BY и DISTINCT NULL считаются одинаковыми: все строки с NULL попадают в одну группу, DISTINCT оставляет один NULL. В ORDER BY NULL оказываются в начале или в конце в зависимости от СУБД; в PostgreSQL положение задают явно через
NULLS FIRSTилиNULLS LAST. -
Для чего нужны COALESCE и NULLIF?
COALESCE(a, b, c)возвращает первое не-NULL значение — так подставляют значения по умолчанию.NULLIF(a, b)возвращает NULL, если a = b, иначе a. Классический приём против деления на ноль:revenue / NULLIF(orders_count, 0).
Подзапросы, CTE и оконные функции
-
Что такое коррелированный подзапрос?
Подзапрос, который ссылается на колонки внешнего запроса и логически выполняется для каждой его строки:
WHERE s1.salary > (SELECT AVG(s2.salary) FROM staff s2 WHERE s2.dept_id = s1.dept_id). Оптимизатор часто переписывает такой запрос в соединение, но на больших таблицах его стоит проверить по плану. -
Чем EXISTS отличается от IN?
INсравнивает значение со списком или результатом подзапроса,EXISTSпроверяет, вернул ли подзапрос хоть одну строку, и может остановиться на первой. Современные оптимизаторы часто строят для них одинаковый план; принципиальная разница — в поведенииNOT INиNOT EXISTS, когда в данных есть NULL. -
Что такое CTE и рекурсивный CTE?
WITH name AS (…)— именованный подзапрос в начале запроса: читается проще вложенных подзапросов, на него можно сослаться несколько раз.WITH RECURSIVEобходит иерархии — дерево категорий, оргструктуру. В PostgreSQL до 12-й версии CTE всегда материализовался и мешал оптимизации; начиная с 12-й CTE, на который ссылаются один раз, встраивается в основной запрос. -
Чем оконные функции отличаются от GROUP BY?
GROUP BY схлопывает строки в одну на группу, а оконная функция считает агрегат или ранг по «окну» и оставляет все строки:
SUM(amount) OVER (PARTITION BY client_id)добавит к каждому заказу сумму всех заказов клиента.PARTITION BYделит строки на группы,ORDER BYвнутри OVER задаёт порядок. -
ROW_NUMBER, RANK и DENSE_RANK — в чём разница?
Для значений 100, 100, 90
ROW_NUMBERдаст 1, 2, 3 — номера всегда уникальны;RANK— 1, 1, 3, с пропуском после равных;DENSE_RANK— 1, 1, 2, без пропусков. Для «N-го по величине значения» нужен DENSE_RANK, для «ровно N строк» — ROW_NUMBER. -
Что делают LAG и LEAD?
Берут значение из предыдущей или следующей строки окна:
revenue - LAG(revenue) OVER (ORDER BY month)— изменение выручки к прошлому месяцу. Для первой строки LAG вернёт NULL, если не задать значение по умолчанию третьим аргументом. -
Что такое рамка окна и почему ROWS и RANGE дают разный результат?
Если в OVER есть ORDER BY, рамка по умолчанию —
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: строки с одинаковым значением сортировки входят в неё вместе. Поэтому у нескольких заказов с одной датой накопительный итог окажется одинаковым. Для построчного итога пишутROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWи добавляют в сортировку уникальное поле.
Индексы, транзакции и нормализация
-
Что такое индекс и как он устроен?
Чаще всего это B-дерево: отсортированная сбалансированная структура, по которой база находит строки за логарифмическое время. Подходит для равенства, диапазонов и сортировки. Индекс ускоряет чтение, но занимает место и замедляет INSERT, UPDATE и DELETE — его тоже нужно обновлять.
-
Как работает составной индекс и важен ли порядок колонок?
Важен. Индекс по
(a, b)помогает условиям поaи поaсbвместе, но, как правило, не помогает условию только поb: данные в нём отсортированы сначала по a. Первой обычно ставят колонку, по которой фильтруют на равенство. -
Почему запрос не использует индекс?
Функция или выражение над колонкой в условии (
WHERE LOWER(email) = …— нужен индекс по выражению),LIKE '%abc'с процентом в начале, неявное приведение типов, низкая селективность — когда условию подходит большая доля таблицы, прочитать её целиком дешевле. План смотрят черезEXPLAIN, реальное время — черезEXPLAIN ANALYZE. -
Чем кластерный индекс отличается от некластерного?
Кластерный определяет физический порядок строк таблицы, поэтому он может быть только один; в SQL Server и в InnoDB (MySQL) это обычно первичный ключ. Некластерный — отдельная структура со ссылками на строки. В PostgreSQL таблица хранится без упорядочивания, и все индексы по сути некластерные; команда
CLUSTERупорядочивает таблицу один раз. -
Что такое ACID?
Atomicity — транзакция выполняется целиком или не выполняется вовсе. Consistency — после неё данные соответствуют всем ограничениям. Isolation — параллельные транзакции не мешают друг другу в пределах уровня изоляции. Durability — зафиксированные изменения не теряются даже при сбое.
-
Какие бывают уровни изоляции и аномалии?
Read Uncommitted допускает грязное чтение — видны незафиксированные данные. Read Committed его убирает, но остаётся неповторяющееся чтение: повторный SELECT видит чужие изменения. Repeatable Read убирает и его, по стандарту остаются фантомы — новые строки. Serializable даёт результат как при последовательном выполнении. По умолчанию в PostgreSQL — Read Committed, в MySQL (InnoDB) — Repeatable Read.
-
Что такое нормализация и зачем денормализация?
Нормализация убирает избыточность: 1НФ — атомарные значения, 2НФ — нет зависимости от части составного ключа, 3НФ — нет зависимостей между неключевыми полями. Так данные не расходятся при обновлении. Денормализация — сознательное дублирование ради скорости чтения: витрины для отчётов, счётчики, заранее посчитанные суммы.
-
Чем первичный ключ отличается от уникального ограничения?
Первичный ключ в таблице один, не допускает NULL и идентифицирует строку. Уникальных ограничений может быть несколько, и NULL в них в большинстве СУБД разрешён, причём не один — ведь NULL не равен NULL. Исключение — SQL Server: там уникальное ограничение допускает только один NULL.
Типовые задачи
-
Найти вторую по величине зарплату
Простой вариант — максимум среди тех, кто меньше максимума. Вариант с
DENSE_RANKлегко обобщить до N-й зарплаты. Если второй зарплаты нет, первый запрос вернёт NULL, а второй — ни одной строки: об этом стоит сказать вслух.SELECT MAX(salary) AS second_salary FROM employees WHERE salary < (SELECT MAX(salary) FROM employees); SELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employees ) t WHERE rnk = 2; -
Найти дубликаты и оставить по одной записи
Найти —
GROUP BYпо полям, которые должны быть уникальными, иHAVING. Удалить — пронумеровать строки внутри группы и стереть все, кроме первой. Сначала уточните у интервьюера, какую запись считать главной: самую раннюю, последнюю или самую полную.SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1; DELETE FROM users WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) t WHERE rn > 1 ); -
Топ-3 товара по выручке в каждой категории
Пронумеровать товары внутри категории оконной функцией и отфильтровать во внешнем запросе — в WHERE того же уровня оконную функцию использовать нельзя.
ROW_NUMBERвернёт ровно три строки на категорию,DENSE_RANK— все товары с тремя наибольшими значениями, включая равные.WITH ranked AS ( SELECT category_id, product_id, revenue, ROW_NUMBER() OVER ( PARTITION BY category_id ORDER BY revenue DESC ) AS rn FROM product_revenue ) SELECT category_id, product_id, revenue FROM ranked WHERE rn <= 3 ORDER BY category_id, rn; -
Посчитать накопительный итог продаж по дням
Сначала агрегировать заказы до дня, потом применить
SUMкак оконную функцию с сортировкой по дате. Рамку лучше указать явно, чтобы не зависеть от поведения RANGE по умолчанию. Для итога внутри каждого магазина или клиента добавьтеPARTITION BY.SELECT order_date, daily_revenue, SUM(daily_revenue) OVER ( ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total FROM ( SELECT order_date, SUM(amount) AS daily_revenue FROM orders GROUP BY order_date ) d ORDER BY order_date;
Прежде чем писать, проговорите план: какие таблицы нужны, как их соединить, где группировать и что будет с NULL и дубликатами. Продуктовые вопросы — метрики, A/B-тесты, retention — собраны в подборке вопросов для аналитика, а требования, интеграции и API — в подборке для системного аналитика.
Как отрепетировать ответы
На собеседовании запрос пишут под взглядом интервьюера и объясняют решение по ходу. Тренируйтесь так же: берите задачу, вслух называйте шаги и только потом пишите код. Почувствовать такой формат поможет бесплатное трёхминутное демо «Первый звонок» в Суфлер AI: ИИ-интервьюер Анна задаёт теоретический вопрос, даёт задачу на live coding и код-ревью, а приложение показывает 2–3 тезиса ответа в окне, которого не видно при демонстрации экрана. Подробнее — на странице скрытого помощника на собеседовании.
Частые вопросы
Какую СУБД учить к собеседованию?
Ту, что указана в вакансии. Если не указана — PostgreSQL: синтаксис близок к стандарту, есть оконные функции, CTE и подробный EXPLAIN. Основа — SELECT, JOIN, группировка, окна — почти одинакова во всех СУБД, различаются детали вроде LIMIT и TOP или функций для работы с датами.
Нужно ли уметь читать план запроса?
Джуну достаточно знать, что такое EXPLAIN, и отличать полное чтение таблицы от поиска по индексу. От мидла ждут больше: сравнить ожидаемое и реальное число строк в EXPLAIN ANALYZE, найти шаг, где теряется время, и предложить индекс или другую формулировку запроса.
Чем вопросы для аналитика отличаются от вопросов для разработчика?
Аналитику дают больше задач на выборку: агрегаты, оконные функции, когорты, воронки. Разработчика чаще спрашивают о проектировании схемы, индексах, транзакциях и блокировках, а также о проблеме N+1 запросов при работе через ORM.
Что делать, если забыл синтаксис?
Сказать, что хотите сделать, и написать логику: «здесь нужна оконная функция с разбиением по категории». Интервьюеру важнее ход мысли, чем точное имя функции. Неуверенность в мелочах лучше проговорить, чем молча гадать.
Запросы на собеседовании запускают или пишут «на бумаге»?
По-разному: в общем документе, в онлайн-редакторе без запуска, иногда на настоящей базе. Поэтому тренируйтесь писать без автодополнения и проверять запрос глазами на маленьком примере — три-четыре строки в каждой таблице, включая NULL и дубликаты.