Аналитик написал SELECT * FROM orders, users без WHERE и без JOIN ... ON. В orders 1000 строк, в users 500 строк. Что вернёт запрос?
JOIN-ы — фундамент аналитического SQL. INNER, LEFT, RIGHT, FULL OUTER, CROSS JOIN, а также UNION, INTERSECT, EXCEPT — на собеседовании нужно не просто знать синтаксис, но и понимать, что происходит при дубликатах ключей и NULL-значениях. Задачи на JOIN встречаются на каждом собеседовании без исключения.
Всего в этом разделе 40 вопросов. Каждый — с правильным ответом и кратким разбором теории. Разбито на 8 частей по 5 вопросов.
1Аналитик написал `SELECT * FROM orders, users` без `WHERE` и без `JOIN ... ON`. В `orders` 1000 строк, в `users` 500 строк. Что вернёт запрос?
AЗапрос упадёт с ошибкой `SYNTAX ERROR`: отсутствие условия `JOIN` блокирует выполнение
BЗапрос вернёт строки `orders`, у которых `user_id` совпадает с `id` клиента в `users`
CЗапрос вернёт строки `orders` с `NULL` вместо данных клиента: поведение аналога `LEFT JOIN`
DКаждая строка `orders` соединится с каждой строкой `users` через неявный `CROSS JOIN`
Ответ: Перечисление таблиц через запятую без условия — это неявный `CROSS JOIN`, который создаёт декартово произведение: 1000 × 500 = 500 000 строк.
Синтаксис `FROM a, b` без `WHERE` эквивалентен `FROM a CROSS JOIN b` — каждая строка первой таблицы соединяется с каждой строкой второй. Это классическая ошибка: аналитик забыл указать `WHERE orders.user_id = users.id`. Результат — 500 000 строк вместо ожидаемых 1000. Всегда проверяйте наличие условия соединения через `ON` или `WHERE`.
2В отчёте нужно вывести всех пользователей и количество их заказов, включая тех, у кого заказов нет. Какой тип соединения между `users` и `orders` по `user_id` нужен?
A`INNER JOIN users` и `orders` по `user_id` сохраняет лишь пользователей с заказами
B`LEFT JOIN users` и `orders` по `user_id` сохраняет всех пользователей из `users`
C`RIGHT JOIN users` и `orders` по `user_id` сохраняет лишь заказы и теряет пользователей без них
D`UNION` таблиц `users` и `orders` объединяет два списка строк без сопоставления по ключу
Ответ: `LEFT JOIN` сохраняет все строки из левой таблицы и подставляет `NULL` для отсутствующих совпадений справа.
`INNER JOIN` вернёт только пользователей, у которых нашлись заказы, и потеряет тех, у кого их нет. Для аналитических отчётов часто важно показать и пользователей без заказов, чтобы их метрика была равна нулю. Для этого берут таблицу `users` слева и делают `LEFT JOIN` к таблице `orders` по `user_id`. `RIGHT JOIN` решает обратную задачу, а `UNION` объединяет результаты двух запросов и не выполняет сопоставление строк по ключу.
3Нужно построить отчёт: по каждому продукту и каждому дню месяца — сумма продаж, включая дни с нулевыми продажами. Как сформировать каркас из всех пар дата–продукт?
AИспользовать `CROSS JOIN` между таблицей дат и таблицей продуктов: декартово произведение всех пар
BИспользовать `INNER JOIN` между таблицей дат и таблицей продуктов по подходящему общему ключу
CИспользовать `LEFT JOIN` таблицы продуктов к таблице дат по столбцу `created_at` для каждой даты
DИспользовать `UNION ALL` таблицы дат и таблицы продуктов в один общий результат запроса
Ответ: `CROSS JOIN` даёт декартово произведение: каждая дата сочетается с каждым продуктом, и получается полный каркас для `LEFT JOIN` к продажам.
`CROSS JOIN` создаёт все возможные комбинации строк двух таблиц: если дат 30 и продуктов 5, получится 150 строк. Затем этот каркас соединяют через `LEFT JOIN` с таблицей продаж — для дней без продаж будут `NULL`, которые заменяют на 0 через `COALESCE`. `INNER JOIN` без ключа связи для пар дат и продуктов смысла не имеет, а `UNION ALL` просто склеивает строки и не порождает пар. Это стандартный паттерн «date spine» в аналитике.
4Первый набор содержит значения: `100, 100, 100, 200`. Второй набор: `100, 300`. Чем отличаются результаты `EXCEPT` и `EXCEPT ALL`?
A`EXCEPT` вернёт `{200}`, а `EXCEPT ALL` вернёт `{100, 100, 200}` с учётом кратности повторяющихся значений
BОба запроса вернут `{200}` без `100`: дубликаты `100, 100, 100` убираются автоматически перед операцией вычитания
CОба запроса вернут `{100, 100, 100, 200}` целиком: оператор `EXCEPT` не убирает дубликаты ни в одном из вариантов
D`EXCEPT` вернёт `{100, 200}`, а `EXCEPT ALL` вернёт пустой набор из-за полного пересечения наборов значений
Ответ: `EXCEPT` дедуплицирует и вычитает множества — вернёт `{200}`. `EXCEPT ALL` учитывает кратность: из 3 копий `100` вычтёт 1, оставит `{100, 100, 200}`.
`EXCEPT` работает как множественное вычитание: сначала дедуплицирует оба набора, затем убирает совпадения. Уникальные значения первого набора — `{100, 200}`; `100` есть и во втором, поэтому остаётся `{200}`. `EXCEPT ALL` учитывает кратность: 3 копии `100` минус 1 копия `100` дают 2 копии `100`, плюс `200` — итого `{100, 100, 200}`. Это аналогично разнице между `UNION` и `UNION ALL`: версия без `ALL` всегда дедуплицирует, а с `ALL` — сохраняет повторы.
5Два запроса: `SELECT * FROM orders JOIN users ON orders.user_id = users.user_id` и `SELECT * FROM orders JOIN users USING(user_id)`. В чём ключевое ограничение `USING`?
A`USING(col)` требует, чтобы столбец соединения назывался одинаково в обеих таблицах
B`USING(col)` соединяет столбцы с разными именами в двух таблицах напрямую без псевдонимов
C`USING(col)` работает только с `INNER JOIN` и недоступен для внешних соединений `LEFT JOIN`
D`USING(col)` подбирает столбец автоматически по совпадению типов данных через системный каталог
Ответ: `USING(col)` — сокращённый синтаксис для `ON`, который требует одинакового имени столбца в обеих таблицах.
`USING(col)` эквивалентен `ON a.col = b.col`, но с двумя отличиями: столбец должен называться одинаково в обеих таблицах, и в результате `SELECT *` этот столбец появится один раз вместо двух. Если в одной таблице столбец называется `user_id`, а в другой `id`, `USING` не подойдёт — нужен `ON`. При этом `USING` работает и с внешними соединениями `LEFT JOIN`/`RIGHT JOIN`, а не только с внутренними.