← Все публикации

SQL на собеседовании: вопросы и задачи, которые помогут понять, а не зазубрить

Interview Ninja

Легко запомнить, что LEFT JOIN сохраняет строки левой таблицы. Но что вернёт запрос, если у одного клиента три заказа? Если условию соединения соответствуют все три, в результате будет три строки с этим клиентом. Сохранить строку не значит оставить её в единственном экземпляре.

На SQL-собеседовании могут спросить и определения, и небольшие задачи. Попробуем разобраться не только в ответах, но и в том, как проверить их на данных и объяснить свои действия.

SQL используют разработчики, аналитики, тестировщики и другие специалисты. Нужная глубина зависит от вакансии и команды: например, в одной роли важнее транзакции, в другой группировки или проверка данных. В примерах ниже используем PostgreSQL. Уточните СУБД на собеседовании: синтаксис и отдельные правила могут различаться.

Кандидат объясняет решение на собеседовании, вокруг него парят термины SQL.

Иллюстрация подготовки к SQL-собеседованию.


Базовые теоретические вопросы по SQL

Попробуйте ответить своими словами, а затем придумать небольшой пример. Один и тот же вопрос разные интервьюеры могут задавать с разной целью: ниже указаны возможные направления проверки, а не универсальные ожидания.

В чем разница между SQL и СУБД?

  • Суть ответа: SQL (Structured Query Language) представляет собой декларативный язык запросов, с помощью которого вы описываете, какие данные хотите получить или изменить. СУБД (система управления базами данных, например PostgreSQL или MySQL) является программным комплексом, который хранит данные на диске, управляет памятью, строит план выполнения запроса, следит за транзакциями и отдает итоговый результат.
  • Что могут проверять: различаете ли вы язык и систему, которая выполняет запрос. Один и тот же запрос может работать в разных СУБД с разной скоростью, а иногда его синтаксис придётся изменить.

Первичный (Primary Key) и внешний (Foreign Key) ключи

  • Суть ответа: первичный ключ однозначно идентифицирует строку в рамках таблицы, гарантирует уникальность и не может содержать значение NULL. Внешний ключ связывает столбец или набор столбцов с подходящим ключом той же или другой таблицы и помогает поддерживать ссылочную целостность. Действия при удалении связанной строки зависят от настройки ограничения: например, удаление может быть запрещено или связанные строки могут удалиться каскадом.
  • Уточняющий вопрос на собеседовании: "Может ли внешний ключ содержать NULL?". Да, может, если поле не объявлено как NOT NULL. Это означает отсутствие связи для конкретной строки.

Чем фильтрация в WHERE отличается от HAVING?

  • Суть ответа: WHERE фильтрует отдельные строки до того, как они будут объединены в группы. HAVING отсекает уже сформированные группы после вычисления агрегатных функций.
  • Типичная ошибка: попытка написать WHERE COUNT(*) > 5. Агрегат относится к группе, а не к отдельной входной строке. В PostgreSQL такое использование агрегата в WHERE вызовет ошибку; для условия на группу здесь нужен HAVING.

Поведение INNER JOIN и LEFT JOIN

  • Суть ответа: INNER JOIN оставляет в итоговой выборке только те пары строк, для которых условие соединения истинно. Если клиенту не соответствует ни один заказ, клиент исчезнет из ответа. LEFT JOIN гарантированно сохраняет каждую строку левой таблицы. Если справа соответствий нет, поля правой таблицы заполняются значениями NULL.
  • Что меняется при изменении данных: подробнее этот механизм разобран в официальном руководстве PostgreSQL по JOIN. Если у клиента справа найдено пять заказов, строка клиента повторится пять раз.

Особенности работы с NULL

  • Суть ответа: NULL означает отсутствие значения или неизвестность, а не пустую строку и не ноль. Из-за трехзначной логики (TRUE, FALSE, UNKNOWN) прямое сравнение val = NULL или val != NULL всегда возвращает NULL (неизвестность), поэтому в WHERE такая строка не проходит: фильтр оставляет только строки, для которых условие истинно.
  • Как проверять: проверка выполняется через операторы IS NULL или IS NOT NULL, как указано в документации PostgreSQL по функциям сравнения.

В чем разница между COUNT(*) и COUNT(column)?

  • Суть ответа: COUNT(*) подсчитывает общее количество строк в выборке или группе, независимо от того, какие значения находятся внутри. COUNT(column) подсчитывает только те строки, где значение указанного столбца не равно NULL.
  • Где это критично: при подсчете связей через LEFT JOIN. Подробный разбор агрегатов приведен в разделе PostgreSQL об агрегатных функциях.

Принцип GROUP BY

  • Суть ответа: GROUP BY сжимает множество строк с одинаковыми значениями в одну итоговую строку для каждой группы.
  • Как объяснить: если нужно вывести имя клиента и число его заказов, можно сгруппировать строки по идентификатору и имени клиента, а затем посчитать заказы внутри каждой группы. В примере ниже оба поля указаны в GROUP BY; возможности опустить зависимый от первичного ключа столбец зависят от правил СУБД.

Зачем нужны индексы и транзакции?

  • Индексы: специальная структура данных (чаще всего B-дерево), ускоряющая поиск и фильтрацию строк ценой дополнительного места на диске и замедления операций записи (INSERT, UPDATE, DELETE). Сам по себе индекс не гарантирует ускорение любого запроса: планировщик СУБД может проигнорировать его, если посчитает чтение всей таблицы более дешевым.
  • Транзакции: позволяют объединить операции в единицу работы. Атомарность означает, что изменения фиксируются вместе либо откатываются. При параллельном доступе поведение зависит в том числе от уровня изоляции; одна атомарность не устраняет все конфликты.

Практические задачи: проверяем логику на едином наборе данных

Разберем типовые sql задачи с собеседований. Перед тем как читать готовый SQL-код и объяснение, посмотрите на таблицы и попробуйте мысленно предсказать результат.

Таблица customers:

  • id = 1, name = "Анна"
  • id = 2, name = "Борис"
  • id = 3, name = "Вера"

Таблица orders:

  • id = 101, customer_id = 1, amount = 100
  • id = 102, customer_id = 1, amount = 150
  • id = 103, customer_id = 2, amount = 50

Столбцы id в обеих таблицах уникальны и объявлены как NOT NULL. Поле orders.customer_id ссылается на customers.id. У Веры заказов нет.

Задача 1. Посчитать количество заказов для каждого клиента

Сначала предскажите: сколько заказов должно оказаться в итоговой таблице у Веры: 0 или 1?

SELECT 
    c.id,
    c.name,
    COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;

Ожидаемый результат:

  • 1 | Анна | 2
  • 2 | Борис | 1
  • 3 | Вера | 0

Разбор по шагам:

  1. Мы объединяем таблицы через LEFT JOIN. Для Веры в правой части нет совпадений, поэтому строка формируется со значениями NULL в полях таблицы orders: (3, 'Вера', NULL, NULL, NULL).
  2. Функция COUNT(o.id) считает только непустые значения. Поскольку o.id для строки Веры равен NULL, счетчик дает 0.
  3. Если бы мы написали COUNT(*), запрос посчитал бы количество строк в сформированной группе. Для Веры существует одна объединенная строка, и результат стал бы ошибочным: 1 вместо 0. Обратитесь к описанию функций агрегации PostgreSQL, чтобы закрепить это различие.
  4. Конструкция ORDER BY c.id гарантирует воспроизводимый порядок строк в ответе.

Задача 2. Найти клиентов, у которых нет ни одного заказа

Попробуйте сформулировать фильтр вслух до того, как посмотрите на условие.

SELECT 
    c.id,
    c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL
ORDER BY c.id;

Ожидаемый результат:

  • 3 | Вера

Разбор по шагам и рассуждения вслух:

  • Можно объяснить так: "Соединяю всех клиентов с заказами, затем оставляю строки без совпавшего заказа". Проверяем o.id, потому что в исходной таблице этот ключ не бывает NULL. Значит, NULL в результате показывает отсутствие совпадения, а не пустое значение в найденном заказе.

Задача 3. Фильтрация правой таблицы: условие в ON против условия в WHERE

Сравним две задачи: показать всех клиентов и только подходящие заказы или оставить лишь строки с заказами строго больше 70.

Что вернет следующий запрос для Бориса и Веры?

Вариант А: фильтр в секции ON

SELECT 
    c.name,
    o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.amount > 70
ORDER BY c.id, o.amount;

Результат варианта А:

  • Анна | 100
  • Анна | 150
  • Борис | NULL
  • Вера | NULL

Почему остался Борис? Условие AND o.amount > 70 в блоке ON определяет лишь то, какие строки справа мы берем в соединение. Заказ Бориса на сумму 50 не подошел под критерий, поэтому правая часть для него обратилась в NULL. Но так как соединение LEFT JOIN, сам Борис из левой таблицы не исчез.

Вариант Б: фильтр перенесен в WHERE

SELECT 
    c.name,
    o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.amount > 70
ORDER BY c.id, o.amount;

Результат варианта Б:

  • Анна | 100
  • Анна | 150

Почему исчезли Борис и Вера? При соединении у Бориса нашёлся заказ на 50, а у Веры совпадения нет, поэтому в её строке поля заказа содержат NULL. Условие o.amount > 70 в WHERE не пропускает ни 50, ни NULL: второе сравнение даёт неизвестный результат. В этом конкретном запросе эффект совпадает с INNER JOIN при таком же условии; не любое условие в WHERE обладает этим свойством.

Полезно проговорить отличие: "В варианте А я сохраняю всех клиентов и подбираю подходящие заказы. В варианте Б оставляю только строки, где нашёлся заказ с суммой больше 70".


Задача 4. Найти клиентов с двумя заказами или больше

Попробуйте сначала объяснить, почему условие на количество заказов относится к группе, а не к отдельной строке.

SELECT c.id, c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
HAVING COUNT(o.id) >= 2
ORDER BY c.id;

Результат: 1 | Анна | 2. После соединения мы группируем строки по клиенту, считаем заказы и с помощью HAVING оставляем группы с двумя заказами или больше. Если бы условие касалось суммы каждого заказа до подсчёта, его место и влияние на клиентов без заказов нужно было бы обсудить отдельно.

Как рассуждать вслух: "Хочу получить по одной строке на клиента. Группирую по идентификатору и имени, считаю ненулевые id заказов и затем фильтрую группы".

План самостоятельной тренировки перед собеседованием

Если механически решать десятки однотипных задач на платформах, уверенность на реальном интервью может так и не появиться. Попробуйте другой подход:

  1. Рисуйте таблицы вручную. Возьмите блокнот, нарисуйте 2 таблицы по 3 строки и руками проведите стрелки соединения. Запишите получившуюся промежуточную таблицу до группировки.
  2. Ищите крайние случаи. Спрашивайте себя: что произойдет с запросом, если в таблице будут дубликаты? Что если правая таблица пустая? Что если в поле внешнего ключа пришел NULL?
  3. Рассуждайте вслух. Коротко объясняйте собеседнику, что собираетесь сделать и почему. Например: "Сначала я объединю клиентов с заказами через LEFT JOIN, чтобы не потерять тех, кто еще ничего не купил, а затем отфильтрую группы по агрегату через HAVING".
  4. Закрепляйте смежные темы. Разработчикам полезно повторить работу языка запросов в связке с прикладным кодом: посмотрите вопросы и задачи по Python для собеседования, чтобы уверенно чувствовать себя на стыке логики приложения и базы данных.

Разбор реальных собеседований и работа над ошибками

Когда технический раунд завершен, сложнее всего вспомнить, где именно логика дала сбой: в неверно выбранном типе соединения, формулировке условий в HAVING или в попытке сравнить NULL через обычное равенство.

Если во время встречи вы использовали Interview Ninja, в доступной сохранённой истории можно найти место, где вы обсуждали SQL, и сверить свои заметки. Запись и распознавание могут быть неполными или ошибочными, поэтому отделяйте собственные слова от ответа ИИ и при необходимости дополняйте их по памяти.

В посте о сохранённых чатах и экспорте описано, как сохранить историю в HTML или Markdown. Если истории нет, выпишите схему таблиц и вопросы по памяти. Не передавайте ИИ чужие закрытые задания, персональные данные или секреты компании. Затем можно воспользоваться планом разбора после собеседования и повторить тему без подсказки.

Другие материалы проекта доступны в Telegram-канале Interview Ninja.