Зачем это спрашивают
Тестировщик проверяет данные не только через интерфейс. Нужно убедиться, что заказ действительно записался, что у него правильный пользователь, что после удаления не осталось «осиротевших» строк. Почти любая такая проверка требует связать две таблицы, а значит — 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.
