JOIN: соединяем таблицы

JOIN: соединяем таблицы

JOIN - это когда ты говоришь базе: «соедини мне данные из двух таблиц так, будто это одна история». Данные лежат в разных таблицах не назло тебе, а по правилам нормализации - JOIN собирает их обратно.

А ещё это поход в магазин со списком. Можно взять сто заказов и в цикле для каждого сходить в базу за клиентом - сто отдельных запросов, каждый за одной строчкой. Можно сходить один раз и забрать всё сразу.

В жизниВ коде
сто походов в магазин, каждый раз за буханкойзапрос в базу на каждую строку - проблема N+1
один поход со спискомJOIN: одна поездка в базу за всеми данными
сам список покупокусловие ON: по какому ключу склеивать

К этой аналогии вернёмся ниже, в разделе про N+1 - там она объясняет, почему JOIN не только про удобство, но и про скорость. А те самые ключи, по которым склеиваем таблицы, - это PRIMARY KEY и FOREIGN KEY из урока про ограничения и целостность.

Виды JOIN на диаграммах Венна: INNER, LEFT, RIGHT, FULL OUTER

Пример таблиц

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;
10 000 строк × 10 000 строк = 100 млн строк в памяти. БД упадёт. Избегай без крайней необходимости.

Несколько 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;
БД не будет угадывать, как связать таблицы. Забудешь `ON` - получишь декартово произведение и грустный лог с мильярдами строк.

Мини-задание

  • Выведи пользователей с их заказами (INNER JOIN)
  • Найди пользователей БЕЗ заказов (LEFT JOIN + WHERE)
  • Соедини три таблицы: users → orders → products (два JOIN в одном запросе)
  • Выведи количество заказов на пользователя (JOIN + GROUP BY, подготовка к следующему уроку)

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.