JOIN — это момент, где SQL становится похож на настоящую работу с базой, а не на фильтр одной таблицы
Пока у нас одна таблица заказов, все просто: выбрали строки, отсортировали, посчитали. Но в реальном проекте данные почти никогда не живут в одной таблице. Клиенты отдельно, заказы отдельно, товары отдельно, платежи отдельно, категории отдельно. JOIN нужен, чтобы собрать эти кусочки в один ответ
Разберем на понятном примере: есть клиенты и заказы. Нужно увидеть, кто что купил, а потом найти клиентов без заказов
- Что получится в конце
- Готовим данные
- INNER JOIN: только совпавшие строки
- Алиасы: чтобы запрос не распухал
- Почему Анна появилась два раза
- LEFT JOIN: оставляем всех клиентов
- Ищем клиентов без заказов
- Главная ошибка с LEFT JOIN и WHERE
- JOIN с несколькими таблицами
- Мини-задания
- Когда JOIN не нужен
- Ответы на эти вопросы могут быть для вас полезными
- Что делает JOIN в SQL?
- Чем INNER JOIN отличается от LEFT JOIN?
- Почему после JOIN появляются дубли?
- Почему LEFT JOIN не показывает строки без совпадений?
- Нужно ли всегда писать INNER JOIN полностью?
- Что почитать дальше по SQL
Что получится в конце
Мы напишем два главных запроса
Первый покажет только клиентов, у которых есть заказы:
SELECT c.name, o.amount
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;
Второй покажет всех клиентов, даже если заказов нет:
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
А еще разберем ловушку:
WHERE o.status = 'paid'
после LEFT JOIN может убрать строки без заказов, хотя мы вроде хотели их сохранить
Готовим данные
Создадим таблицу клиентов:
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
Добавим клиентов:
INSERT INTO customers (id, name, city) VALUES
(1, 'Анна', 'Казань'),
(2, 'Игорь', 'Москва'),
(3, 'Мария', 'Самара'),
(4, 'Олег', 'Екатеринбург');
Теперь заказы:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount INTEGER,
status TEXT
);
Добавим строки:
INSERT INTO orders (id, customer_id, amount, status) VALUES
(1, 1, 7900, 'paid'),
(2, 1, 15000, 'paid'),
(3, 2, 4500, 'pending'),
(4, 3, 1200, 'cancelled');
Связь такая: orders.customer_id указывает на customers.id
INNER JOIN: только совпавшие строки
Напишем первый JOIN:
SELECT customers.name, orders.amount, orders.status
FROM customers
INNER JOIN orders ON orders.customer_id = customers.id;
Результат:
| Анна | 7900 | paid |
|---|---|---|
| Анна | 15000 | paid |
| Игорь | 4500 | pending |
| Мария | 1200 | cancelled |
Олега нет, потому что у него нет заказа. INNER JOIN возвращает только те строки, где нашлась пара с обеих сторон
Алиасы: чтобы запрос не распухал
В реальных запросах имена таблиц часто длиннее, поэтому используют алиасы:
SELECT c.name, o.amount, o.status
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;
customers c означает: дальше таблицу customers можно называть c. orders o — таблицу orders можно называть o
Алиасы не только сокращают код, но и снимают неоднозначность. Если в двух таблицах есть столбец id, лучше явно писать c.id и o.id
Почему Анна появилась два раза
У Анны два заказа. JOIN не "склеивает клиента в одну строку", он показывает все найденные пары:
- Анна + заказ 1;
- Анна + заказ 2.
Это не ошибка. Это нормальное поведение соединения
Если нужна одна строка на клиента, добавляем группировку:
SELECT c.name, COUNT(o.id) AS orders_count, SUM(o.amount) AS total_amount
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
Результат:
| Анна | 2 | 22900 |
|---|---|---|
| Игорь | 1 | 4500 |
| Мария | 1 | 1200 |
Олега все еще нет, потому что это INNER JOIN
LEFT JOIN: оставляем всех клиентов
Теперь нужно увидеть всех клиентов, включая тех, у кого нет заказов
SELECT c.name, o.amount, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
Результат:
| Анна | 7900 | paid |
|---|---|---|
| Анна | 15000 | paid |
| Игорь | 4500 | pending |
| Мария | 1200 | cancelled |
| Олег | NULL | NULL |
LEFT JOIN берет все строки из левой таблицы, то есть из customers. Если справа в orders ничего не нашлось, база подставляет NULL
Это очень полезно для задач:
- найти клиентов без заказов;
- найти товары без продаж;
- найти статьи без комментариев;
- найти пользователей без активной подписки.
Ищем клиентов без заказов
После LEFT JOIN можно оставить только строки, где заказ не найден:
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Результат:
| id | name |
|---|---|
| 4 | Олег |
Мы говорим: покажи всех клиентов, для которых правая сторона соединения пустая
Главная ошибка с LEFT JOIN и WHERE
Допустим, хотим вывести всех клиентов и их оплаченные заказы. Новичок часто пишет так:
SELECT c.name, o.amount, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';
На первый взгляд логично. Но есть проблема: WHERE o.status = 'paid' убирает строки, где o.status равен NULL. То есть клиенты без заказов пропадут
Если нужно сохранить всех клиентов, условие по правой таблице лучше перенести в ON:
SELECT c.name, o.amount, o.status
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.status = 'paid';
Теперь результат будет таким:
| Анна | 7900 | paid |
|---|---|---|
| Анна | 15000 | paid |
| Игорь | NULL | NULL |
| Мария | NULL | NULL |
| Олег | NULL | NULL |
Мы оставили всех клиентов, но справа подтянули только оплаченные заказы
JOIN с несколькими таблицами
Добавим товары:
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT
);
Для простоты представим, что в заказе есть product_id. Тогда запрос может выглядеть так:
SELECT c.name AS customer_name, p.name AS product_name, o.amount
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id;
SQL читается как цепочка связей. Заказ связан с клиентом по customer_id, заказ связан с товаром по product_id
Главное не пытаться соединять таблицы "потому что они рядом". У каждого JOIN должно быть понятное условие связи
Мини-задания
- Выведи всех клиентов и их заказы через
LEFT JOIN. - Найди клиентов без заказов.
- Найди клиентов, у которых есть только неоплаченные или отмененные заказы.
- Посчитай количество заказов по каждому клиенту.
- Перенеси условие
o.status = 'paid'изWHEREвONи сравни результат.
Возможное решение четвертого задания:
SELECT c.name, COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders_count DESC;
Здесь LEFT JOIN нужен, чтобы клиент без заказов тоже попал в отчет с количеством 0
Когда JOIN не нужен
Иногда человек тянется к JOIN, хотя ему нужен только факт существования связи. Например: "покажи клиентов, у которых есть хотя бы один оплаченный заказ". Это можно сделать через JOIN, но часто лучше подходит EXISTS
SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
AND o.status = 'paid'
);
Если не нужны поля из orders, а нужен только да/нет-фильтр, EXISTS может быть чище
Ответы на эти вопросы могут быть для вас полезными
Что делает JOIN в SQL?
JOIN соединяет строки из разных таблиц по условию связи. Например, заказ можно связать с клиентом через orders.customer_id = customers.id
Чем INNER JOIN отличается от LEFT JOIN?
INNER JOIN возвращает только совпавшие строки. LEFT JOIN сохраняет все строки из левой таблицы и подставляет NULL, если справа ничего не нашлось
Почему после JOIN появляются дубли?
Это не всегда дубли. Если у одного клиента несколько заказов, соединение вернет несколько пар "клиент + заказ". Чтобы получить одну строку на клиента, нужна группировка или другая логика
Почему LEFT JOIN не показывает строки без совпадений?
Частая причина — условие по правой таблице стоит в WHERE. Оно убирает строки с NULL. Если нужно сохранить левую таблицу полностью, часть условий стоит переносить в ON
Нужно ли всегда писать INNER JOIN полностью?
Во многих базах JOIN без уточнения означает INNER JOIN. Но в учебных материалах я бы писал явно: так легче читать запрос и меньше путаницы
Что почитать дальше по SQL
Если вы собираете тему по шагам, рядом лучше открыть:
- SQL с нуля: первый запрос, фильтр и сортировка — закрепить базовый SELECT перед соединениями.
- EXISTS в SQL на простых примерах — сравнить JOIN с EXISTS и NOT EXISTS.
- GROUP BY и HAVING: группировка без каши — посчитать итоги после соединения таблиц.
- Оконные функции SQL: первый нормальный пример — оставить строки на месте и добавить аналитические расчеты.



