Условие
Есть две таблицы:
customers(customer_id, name, city) -- 100 строк
orders(order_id, customer_id, amount, dt) -- 1000 строк, некоторые customer_id отсутствуют в customers
Напишите запросы:
- Сколько строк вернёт INNER / LEFT / RIGHT / FULL JOIN customers и orders по
customer_id? - Клиенты, у которых нет ни одного заказа.
- Заказы от клиентов, которых нет в
customers(orphan orders). - Декартово произведение (зачем оно вообще нужно?).
Решение
Подход
Пусть в customers 100 строк, в orders 1000, из которых 50 строк имеют customer_id, отсутствующий в customers. У 30 клиентов нет заказов.
| JOIN | Строк | Что включает |
|---|---|---|
| INNER | 950 | только совпавшие в обеих |
| LEFT | 950 + 30 = 980 | + клиенты без заказов |
| RIGHT | 950 + 50 = 1000 | + сиротские заказы |
| FULL | 950 + 30 + 50 = 1030 | оба «хвоста» |
| CROSS | 100 × 1000 = 100К | все пары |
Запросы
1. Демонстрация
-- INNER
SELECT COUNT(*) FROM customers c INNER JOIN orders o ON o.customer_id = c.customer_id;
-- LEFT
SELECT COUNT(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id;
-- FULL
SELECT COUNT(*) FROM customers c FULL JOIN orders o ON o.customer_id = c.customer_id;2. Клиенты без заказов
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL; -- ключевой признак «нет совпадения»
-- альтернатива через NOT EXISTS (часто быстрее):
SELECT c.customer_id, c.name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);3. Orphan orders
SELECT o.order_id, o.customer_id, o.amount
FROM orders o
LEFT JOIN customers c ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;
-- NOT EXISTS:
SELECT o.*
FROM orders o
WHERE NOT EXISTS (SELECT 1 FROM customers c WHERE c.customer_id = o.customer_id);4. CROSS JOIN — реальный кейс
Календарная матрица «город × дата» для plot fill:
SELECT c.city, d.dt, COALESCE(SUM(o.amount), 0) AS gmv
FROM (SELECT DISTINCT city FROM customers) c
CROSS JOIN generate_series(date '2025-01-01', date '2025-12-31', INTERVAL '1 day') d(dt)
LEFT JOIN customers cu ON cu.city = c.city
LEFT JOIN orders o ON o.customer_id = cu.customer_id
AND o.dt::date = d.dt::date
GROUP BY c.city, d.dt
ORDER BY c.city, d.dt;— чтобы в дашборде не было «дырок» для дней без продаж.
Подводные камни
WHERE o.amount > 0на LEFT JOIN превращает его в INNER: NULL-строки фильтруются. Условия по правой таблице — вON, не вWHERE.
-- Правильно: NULL-клиенты сохраняются
LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.amount > 0
-- Неправильно:
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.amount > 0-
FULL JOIN не существует в MySQL — эмулируется через
LEFT UNION RIGHT. -
NOT IN+ NULL ломается:
SELECT * FROM customers
WHERE customer_id NOT IN (SELECT customer_id FROM orders);
-- если в orders есть NULL — вернёт 0 строк!Использовать NOT EXISTS или WHERE customer_id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL).
-
CROSS JOIN на больших таблицах: 1M × 1M = 1 трлн строк — никогда не запускать без предварительной фильтрации.
-
Cartesian product «по ошибке»: забыли
ONили ON-условие всегдаTRUE→ CROSS JOIN. Результат: 10 минут + OOM. -
USING (col)vsON a.col = b.col: с USING колонкаcolпоявляется один раз. Удобно, но иногда теряется явность.
Эталонный ответ
INNER 950, LEFT 980, RIGHT 1000, FULL 1030, CROSS 100000.
Клиенты без заказов: LEFT JOIN + WHERE order_id IS NULL либо NOT EXISTS.
Orphan orders: симметрично — LEFT JOIN customers + WHERE c.customer_id IS NULL.
CROSS JOIN — для календарных матриц / комбинаторных задач.
Главное: условие по правой таблице в LEFT JOIN — в ON, не в WHERE. И всегда NOT EXISTS вместо NOT IN при возможном NULL.