JOIN: соединяем таблицы
JOIN: соединяем таблицы
JOIN - это когда ты говоришь базе: «соедини мне данные из двух таблиц так, будто это одна история». Данные лежат в разных таблицах не назло тебе, а по правилам нормализации - JOIN собирает их обратно.
А ещё это поход в магазин со списком. Можно взять сто заказов и в цикле для каждого сходить в базу за клиентом - сто отдельных запросов, каждый за одной строчкой. Можно сходить один раз и забрать всё сразу.
| В жизни | В коде |
|---|---|
| сто походов в магазин, каждый раз за буханкой | запрос в базу на каждую строку - проблема N+1 |
| один поход со списком | JOIN: одна поездка в базу за всеми данными |
| сам список покупок | условие ON: по какому ключу склеивать |
К этой аналогии вернёмся ниже, в разделе про N+1 - там она объясняет, почему JOIN не только про удобство, но и про скорость. А те самые ключи, по которым склеиваем таблицы, - это PRIMARY KEY и FOREIGN KEY из урока про ограничения и целостность.
Пример таблиц
users (левая таблица):
id | name
1 | Alice
2 | Bob
3 | Carol
orders (правая таблица):
id | user_id | total
101 | 1 | 5000
102 | 1 | 3000
103 | 2 | 7000
(Carol - без заказов)
Table aliases - сокращение имён
Пиши u вместо users, o вместо orders - это alias:
SELECT u.id, u.name, o.id AS order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
Конвенция: используй первые буквы (u для users, o для orders). Упрощает чтение и печать.
INNER JOIN (JOIN)
Показывает только строки, которые есть в обеих таблицах:
SELECT u.id, u.name, o.id AS order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id;
-- Результат:
-- id | name | order_id | total
-- 1 | Alice | 101 | 5000
-- 1 | Alice | 102 | 3000
-- 2 | Bob | 103 | 7000
-- (Carol не в результате, потому что нет заказов)
LEFT JOIN
Показывает все строки из левой таблицы, даже если в правой нет соответствия:
SELECT u.id, u.name, o.id AS order_id, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
-- Результат:
-- id | name | order_id | total
-- 1 | Alice | 101 | 5000
-- 1 | Alice | 102 | 3000
-- 2 | Bob | 103 | 7000
-- 3 | Carol | NULL | NULL - Carol есть, но заказов нет!
Практика: найти пользователей без заказов:
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL; - заказы не найдены
RIGHT JOIN (реже используется)
Наоборот LEFT JOIN - все из правой таблицы, даже если нет в левой. На практике редко нужен (просто поменяй местами таблицы и используй LEFT).
-- Вместо RIGHT:
-- FROM users u RIGHT JOIN orders o ON ...
-- Пиши:
FROM orders o LEFT JOIN users u ON ...
FULL OUTER JOIN (если БД поддерживает)
Все строки из обеих таблиц:
-- PostgreSQL:
SELECT u.id, u.name, o.id AS order_id
FROM users u
FULL OUTER JOIN orders o ON o.user_id = u.id;
-- MySQL этого не поддерживает, используй:
-- (SELECT ... FROM users LEFT JOIN orders ...)
-- UNION
-- (SELECT ... FROM users RIGHT JOIN orders ...)
CROSS JOIN (опасно!)
Декартово произведение: каждая строка левой умножается на каждую строку правой. Редко нужен:
-- 3 пользователя × 4 заказа = 12 строк (даже если нет связей!)
SELECT u.id, o.id FROM users u CROSS JOIN orders o;
Несколько JOIN в одном запросе
-- users → orders → products
SELECT u.name, o.id AS order_id, p.title
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
ORDER BY u.name;
Порядок JOIN важен (как правило, сверху вниз). Читай слева направо и вниз.
Проблема N+1: сто походов в магазин
Берёшь список заказов - это один запрос. Дальше в цикле для каждого заказа лезешь в базу за клиентом - и это ещё сто запросов. Сто заказов - 101 запрос. Отсюда и название: N+1.
Те самые сто походов в магазин. Каждый раз за одной буханкой, вместо одного похода со списком.
-- Сначала это (один запрос):
SELECT id, user_id, total FROM orders;
-- А потом вот это, по разу на каждый заказ:
SELECT id, name FROM users WHERE id = 1;
SELECT id, name FROM users WHERE id = 2;
SELECT id, name FROM users WHERE id = 3;
-- ...и так сто раз
У себя на машине это 0.2 секунды: база рядом, данных мало, всё летает. В проде заказов не сто, а десять тысяч, и база живёт за сетью.
Самое противное в N+1 - её не видно глазами. Код выглядит нормально, логика правильная. Видно только в логе запросов: одинаковые SELECT с разным id, сотнями подряд.
Индекс тут не спасёт: он ускорит каждый из ста запросов, но их всё равно останется сто.
Лечится другим - JOIN (или preload, если ходишь в базу через ORM). Один запрос вместо ста:
SELECT o.id AS order_id, o.total, u.name
FROM orders o
JOIN users u ON u.id = o.user_id;
Проверить себя просто: посчитай, сколько запросов уходит в базу на одну страницу. Растёт вместе с количеством строк на экране - у тебя N+1.
А как база выполняет сам JOIN - через Nested Loop, Hash Join или Merge Join - видно в плане запроса.
Частая ошибка: забытый ON
-- НЕПРАВИЛЬНО (забыл ON):
SELECT u.id, o.id FROM users u JOIN orders o;
-- Это CROSS JOIN! Каждый юзер ко всем заказам.
-- ПРАВИЛЬНО:
SELECT u.id, o.id FROM users u JOIN orders o ON o.user_id = u.id;
Мини-задание
- Выведи пользователей с их заказами (INNER JOIN)
- Найди пользователей БЕЗ заказов (LEFT JOIN + WHERE)
- Соедини три таблицы: users → orders → products (два JOIN в одном запросе)
- Выведи количество заказов на пользователя (JOIN + GROUP BY, подготовка к следующему уроку)