Вопросы на собеседовании по SQL

Что спрашивают по SQL у стажёров, джунов и мидлов: JOIN и группировка, NULL, подзапросы и оконные функции, индексы и транзакции. В конце — четыре типовые задачи с короткими решениями.

JOIN, группировка и основы

  1. Какие бывают JOIN?

    INNER — только строки, у которых есть пара в обеих таблицах; LEFT — все строки левой таблицы и совпавшие из правой, остальное NULL; RIGHT — наоборот; FULL — все строки обеих; CROSS — все комбинации. Self join — соединение таблицы с самой собой, например сотрудника с его руководителем.

  2. При LEFT JOIN есть разница, писать условие в ON или в WHERE?

    Есть. Условие в ON участвует в соединении: строки левой таблицы сохраняются, просто справа будет NULL. То же условие на правую таблицу в WHERE отбросит строки с NULL — и LEFT JOIN фактически превратится в INNER.

  3. Как найти клиентов без заказов?

    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, результат будет пустым.

  4. В каком порядке выполняется SELECT?

    Логически: FROM и JOIN → WHERE → GROUP BY → HAVING → SELECT, где считаются и оконные функции → DISTINCT → ORDER BY → LIMIT. Поэтому псевдоним из SELECT нельзя использовать в WHERE, а в ORDER BY можно, и оконную функцию нельзя отфильтровать в WHERE того же запроса.

  5. Чем WHERE отличается от HAVING?

    WHERE фильтрует строки до группировки, HAVING — группы после неё, поэтому в нём можно использовать агрегаты: HAVING COUNT(*) > 5. Условие без агрегатов лучше писать в WHERE — группировать придётся меньше строк.

  6. COUNT(*), COUNT(col) и COUNT(DISTINCT col) — в чём разница?

    COUNT(*) считает все строки, COUNT(col) — строки, где col не NULL, COUNT(DISTINCT col) — число разных не-NULL значений. После LEFT JOIN считайте по колонке правой таблицы: COUNT(o.id) даст клиентам без заказов 0, а COUNT(*) — 1.

  7. Чем UNION отличается от UNION ALL?

    UNION убирает дубликаты, и для этого базе приходится сортировать или хешировать результат. UNION ALL просто склеивает наборы и работает быстрее. Если дублей быть не может или они нужны, берите UNION ALL.

  8. Чем DELETE отличается от TRUNCATE и DROP?

    DELETE удаляет строки, можно с условием, при этом срабатывают триггеры на удаление. TRUNCATE очищает таблицу целиком и работает намного быстрее. DROP удаляет саму таблицу вместе со структурой. Откатывается ли TRUNCATE в транзакции, зависит от СУБД: в PostgreSQL — да, в MySQL и Oracle — нет.

NULL

  1. Как NULL ведёт себя в сравнениях?

    NULL означает «неизвестно», и любое сравнение с ним, даже NULL = NULL, даёт не TRUE, а UNKNOWN. WHERE пропускает только TRUE, поэтому col = NULL не находит ничего — нужно IS NULL. По той же причине col <> 'x' не вернёт строки, где col равен NULL.

  2. Как агрегатные функции обрабатывают NULL?

    SUM, AVG, MIN, MAX и COUNT(col) пропускают NULL. AVG делит на число не-NULL значений, а не на число строк. SUM по пустому набору возвращает NULL, а не 0, поэтому пишут COALESCE(SUM(amount), 0).

  3. Как NULL ведёт себя в GROUP BY, DISTINCT и ORDER BY?

    В GROUP BY и DISTINCT NULL считаются одинаковыми: все строки с NULL попадают в одну группу, DISTINCT оставляет один NULL. В ORDER BY NULL оказываются в начале или в конце в зависимости от СУБД; в PostgreSQL положение задают явно через NULLS FIRST или NULLS LAST.

  4. Для чего нужны COALESCE и NULLIF?

    COALESCE(a, b, c) возвращает первое не-NULL значение — так подставляют значения по умолчанию. NULLIF(a, b) возвращает NULL, если a = b, иначе a. Классический приём против деления на ноль: revenue / NULLIF(orders_count, 0).

Подзапросы, CTE и оконные функции

  1. Что такое коррелированный подзапрос?

    Подзапрос, который ссылается на колонки внешнего запроса и логически выполняется для каждой его строки: WHERE s1.salary > (SELECT AVG(s2.salary) FROM staff s2 WHERE s2.dept_id = s1.dept_id). Оптимизатор часто переписывает такой запрос в соединение, но на больших таблицах его стоит проверить по плану.

  2. Чем EXISTS отличается от IN?

    IN сравнивает значение со списком или результатом подзапроса, EXISTS проверяет, вернул ли подзапрос хоть одну строку, и может остановиться на первой. Современные оптимизаторы часто строят для них одинаковый план; принципиальная разница — в поведении NOT IN и NOT EXISTS, когда в данных есть NULL.

  3. Что такое CTE и рекурсивный CTE?

    WITH name AS (…) — именованный подзапрос в начале запроса: читается проще вложенных подзапросов, на него можно сослаться несколько раз. WITH RECURSIVE обходит иерархии — дерево категорий, оргструктуру. В PostgreSQL до 12-й версии CTE всегда материализовался и мешал оптимизации; начиная с 12-й CTE, на который ссылаются один раз, встраивается в основной запрос.

  4. Чем оконные функции отличаются от GROUP BY?

    GROUP BY схлопывает строки в одну на группу, а оконная функция считает агрегат или ранг по «окну» и оставляет все строки: SUM(amount) OVER (PARTITION BY client_id) добавит к каждому заказу сумму всех заказов клиента. PARTITION BY делит строки на группы, ORDER BY внутри OVER задаёт порядок.

  5. 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.

  6. Что делают LAG и LEAD?

    Берут значение из предыдущей или следующей строки окна: revenue - LAG(revenue) OVER (ORDER BY month) — изменение выручки к прошлому месяцу. Для первой строки LAG вернёт NULL, если не задать значение по умолчанию третьим аргументом.

  7. Что такое рамка окна и почему ROWS и RANGE дают разный результат?

    Если в OVER есть ORDER BY, рамка по умолчанию — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: строки с одинаковым значением сортировки входят в неё вместе. Поэтому у нескольких заказов с одной датой накопительный итог окажется одинаковым. Для построчного итога пишут ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW и добавляют в сортировку уникальное поле.

Индексы, транзакции и нормализация

  1. Что такое индекс и как он устроен?

    Чаще всего это B-дерево: отсортированная сбалансированная структура, по которой база находит строки за логарифмическое время. Подходит для равенства, диапазонов и сортировки. Индекс ускоряет чтение, но занимает место и замедляет INSERT, UPDATE и DELETE — его тоже нужно обновлять.

  2. Как работает составной индекс и важен ли порядок колонок?

    Важен. Индекс по (a, b) помогает условиям по a и по a с b вместе, но, как правило, не помогает условию только по b: данные в нём отсортированы сначала по a. Первой обычно ставят колонку, по которой фильтруют на равенство.

  3. Почему запрос не использует индекс?

    Функция или выражение над колонкой в условии (WHERE LOWER(email) = … — нужен индекс по выражению), LIKE '%abc' с процентом в начале, неявное приведение типов, низкая селективность — когда условию подходит большая доля таблицы, прочитать её целиком дешевле. План смотрят через EXPLAIN, реальное время — через EXPLAIN ANALYZE.

  4. Чем кластерный индекс отличается от некластерного?

    Кластерный определяет физический порядок строк таблицы, поэтому он может быть только один; в SQL Server и в InnoDB (MySQL) это обычно первичный ключ. Некластерный — отдельная структура со ссылками на строки. В PostgreSQL таблица хранится без упорядочивания, и все индексы по сути некластерные; команда CLUSTER упорядочивает таблицу один раз.

  5. Что такое ACID?

    Atomicity — транзакция выполняется целиком или не выполняется вовсе. Consistency — после неё данные соответствуют всем ограничениям. Isolation — параллельные транзакции не мешают друг другу в пределах уровня изоляции. Durability — зафиксированные изменения не теряются даже при сбое.

  6. Какие бывают уровни изоляции и аномалии?

    Read Uncommitted допускает грязное чтение — видны незафиксированные данные. Read Committed его убирает, но остаётся неповторяющееся чтение: повторный SELECT видит чужие изменения. Repeatable Read убирает и его, по стандарту остаются фантомы — новые строки. Serializable даёт результат как при последовательном выполнении. По умолчанию в PostgreSQL — Read Committed, в MySQL (InnoDB) — Repeatable Read.

  7. Что такое нормализация и зачем денормализация?

    Нормализация убирает избыточность: 1НФ — атомарные значения, 2НФ — нет зависимости от части составного ключа, 3НФ — нет зависимостей между неключевыми полями. Так данные не расходятся при обновлении. Денормализация — сознательное дублирование ради скорости чтения: витрины для отчётов, счётчики, заранее посчитанные суммы.

  8. Чем первичный ключ отличается от уникального ограничения?

    Первичный ключ в таблице один, не допускает NULL и идентифицирует строку. Уникальных ограничений может быть несколько, и NULL в них в большинстве СУБД разрешён, причём не один — ведь NULL не равен NULL. Исключение — SQL Server: там уникальное ограничение допускает только один NULL.

Типовые задачи

  1. Найти вторую по величине зарплату

    Простой вариант — максимум среди тех, кто меньше максимума. Вариант с 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;
  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. Топ-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;
  4. Посчитать накопительный итог продаж по дням

    Сначала агрегировать заказы до дня, потом применить 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 и дубликаты.