Все разборы

SQL и данные

Как ответить на вопрос «Какие виды JOIN вы знаете» на собеседовании QA

INNER, LEFT, RIGHT, FULL и CROSS JOIN на одном примере: откуда берутся NULL, как найти пользователей без заказов и какие ловушки ждут на живой задаче.

Зачем это спрашивают

Тестировщик проверяет данные не только через интерфейс. Нужно убедиться, что заказ действительно записался, что у него правильный пользователь, что после удаления не осталось «осиротевших» строк. Почти любая такая проверка требует связать две таблицы, а значит — JOIN.

Интервьюер слушает, понимаете ли вы, откуда в результате появляются NULL и почему меняется количество строк. Часто сразу за теорией следует живая задача: найти пользователей без заказов. На ней хорошо видно, умеет ли кандидат применять LEFT JOIN, а не только называть его.

Как построить сильный ответ

Покажите все виды на одном маленьком примере — так ответ получается наглядным и проверяемым.

-- users                       -- orders
-- id | username               -- id | user_id | total | status
--  1 | anna                   -- 10 |       1 |  1500 | paid
--  2 | boris                  -- 11 |       1 |   700 | failed
--  3 | vera                   -- 12 |       2 |  2300 | paid
--                             -- 13 |    NULL |   990 | paid  (гостевой заказ)

INNER JOIN — только пары

Возвращает строки, для которых условие соединения выполнилось с обеих сторон.

SELECT u.username, o.id AS order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;

Результат — три строки: два заказа Анны и один заказ Бориса. Веры нет, потому что у неё нет заказов. Гостевого заказа тоже нет: NULL = 1 не даёт истину. Слово INNER можно не писать.

LEFT JOIN — все строки левой таблицы

Все пользователи плюс найденные заказы. Если пары нет, колонки правой таблицы заполняются NULL.

SELECT u.username, o.id AS order_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

Четыре строки: те же три плюс vera | NULL. Именно так и появляются NULL, о которых спрашивают.

RIGHT JOIN и FULL JOIN

RIGHT JOIN — зеркальный вариант: все заказы, включая гостевой, у которого username будет NULL. На практике его обычно переписывают в LEFT, поменяв таблицы местами. FULL JOIN возвращает всё с обеих сторон: пять строк, включая Веру без заказа и гостевой заказ без пользователя. Удобен для сверки двух источников, когда нужно увидеть расхождения в обе стороны.

CROSS JOIN

Декартово произведение без условия: каждая строка с каждой, здесь 3 × 4 = 12 строк. Пригодится, чтобы сгенерировать комбинации тестовых данных.

Живая задача: пользователи без заказов

SELECT u.id, u.username
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

Проверяем IS NULL по столбцу правой таблицы, который в реальной строке не бывает пустым, — по первичному ключу. Тот же результат через подзапрос:

SELECT u.id, u.username
FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
);

Ловушка: условие в WHERE вместо ON

Нужны все пользователи и только их оплаченные заказы. Если написать WHERE o.status = 'paid', строки с NULL отфильтруются, и LEFT JOIN по факту станет INNER — Вера пропадёт. Условие на правую таблицу ставят в ON:

SELECT u.username, o.id AS order_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid';

Пример ответа

Основные виды — INNER, LEFT, RIGHT, FULL и ещё CROSS. INNER возвращает только строки, у которых нашлась пара в обеих таблицах. LEFT возвращает все строки левой таблицы, а если пары нет, колонки правой заполняются NULL. RIGHT — то же самое зеркально, его обычно заменяют на LEFT. FULL берёт все строки с обеих сторон, удобно для сверки данных. CROSS — каждая строка с каждой. Чаще всего я пользуюсь INNER и LEFT. Например, чтобы найти пользователей без заказов, делаю LEFT JOIN orders и оставляю строки, где id заказа IS NULL, — или пишу NOT EXISTS. Ещё помню, что фильтр по правой таблице при LEFT JOIN надо ставить в ON, иначе он превратится в INNER.

Частые ошибки

  • Писать WHERE o.id = NULL. Сравнение с NULL через = никогда не даёт истину, нужен IS NULL.
  • Решать задачу про пользователей без заказов через INNER JOIN — он такие строки просто не вернёт.
  • Не объяснить, откуда в LEFT JOIN появляются NULL.
  • Ставить фильтр по правой таблице в WHERE и удивляться, что пропали строки.
  • Не замечать размножения строк при связи один-ко-многим и считать суммы с задвоением.

Пример на MarketCove

В учебном магазине https://shop.tomiddle.com заказы и аккаунты хранятся в памяти сервера, но логика сверки та же, что и с базой. Представьте, что данные выгружены в таблицы users и orders, как в примере выше, и вы проверяете оформление заказа.

Какие вопросы задаёт тестировщик через JOIN:

  • у каждого заказа зарегистрированного пользователя есть существующий пользователь — LEFT JOIN users от orders и поиск u.id IS NULL там, где user_id заполнен;
  • гостевые заказы действительно сохраняются без пользователя, а не привязываются к случайному аккаунту;
  • история заказов в профиле совпадает с тем, что находит запрос по этому пользователю, — то же самое можно сверить с ответом /api/orders.

Так SQL становится инструментом проверки гипотез, а не отдельной темой для собеседования. Следующий шаг — посчитать заказы по пользователям и статусам, об этом статья про GROUP BY и HAVING.

Задание для практики

Создайте у себя в PostgreSQL две таблицы из примера и наполните их теми же данными. Выполните все четыре вида JOIN и сверьте количество строк с тем, что указано в статье. Затем напишите три запроса: пользователи без заказов через LEFT JOIN, то же через NOT EXISTS и заказы, у которых указан несуществующий пользователь. Обратите внимание, что в orders уже есть гостевой заказ с user_id = NULL: выполните WHERE u.id NOT IN (SELECT user_id FROM orders) и сравните результат с NOT EXISTS. Потом потренируйтесь отвечать вслух — помогут вопросы по SQL.

Что спросят следом

  • Почему NOT IN может вернуть пустой результат там, где NOT EXISTS находит строки?
    Если подзапрос в NOT IN возвращает хотя бы один NULL, условие для каждой строки становится не истинным, а неизвестным, и строка отбрасывается. NOT EXISTS проверяет только наличие подходящей строки и с NULL так не ломается. Поэтому для поиска записей без пары надёжнее NOT EXISTS или LEFT JOIN с проверкой IS NULL.
  • Почему после JOIN строк стало больше, чем в исходной таблице?
    Потому что связь один-ко-многим: у одного пользователя несколько заказов, и каждая пара попадает в результат отдельной строкой. Это нормально, но при подсчёте сумм и количеств легко посчитать одно и то же несколько раз. Помогает группировка или подсчёт COUNT(DISTINCT ...) по нужному ключу.
  • Что такое self join и где он пригодится?
    Это соединение таблицы с самой собой под разными псевдонимами. Например, сотрудники и их руководители в одной таблице или поиск дублей: пары заказов одного пользователя с одинаковой суммой, созданных с разницей в несколько секунд. Для тестировщика это удобный способ искать задвоения после повторной отправки формы.
  1. Q-01
    Чем WHERE отличается от HAVING? Как работает GROUP BY?
  2. Q-02
    Что вы проверяете при тестировании API?
  3. Q-03
    Как понять, где баг: на фронтенде или на бэкенде?
  4. Q-04
    Что такое Kafka и как тестировать систему с ней?

Отрепетировать ответ с куратором

В сопровождении — весь банк вопросов, мок-собеседование и разбор каждого настоящего собеседования, пока не придёт оффер. Гарантий трудоустройства не даём — готовим к тому, что спросят.