$ sudo teach IT

🔗 Урок 4.2: LEFT JOIN

Модуль 4: JOIN — соединяем таблицы

👋 В прошлом уроке мы соединяли таблицы с INNER JOIN — он показывает только то, что есть в обеих таблицах. Но что, если нужно показать все строки из левой таблицы, даже если в правой нет совпадения? Например, всех клиентов, даже тех, кто ничего не заказал. Или все товары, даже без категории. Тут на помощь приходит LEFT JOIN!

🧠 LEFT JOIN — все строки из левой таблицы

LEFT JOIN работает так:

  • Берёт ВСЕ строки из левой таблицы (той, что в FROM).
  • Для каждой строки ищет совпадение в правой таблице (той, что в JOIN).
  • Если совпадение есть — добавляет данные из правой таблицы (как INNER JOIN).
  • Если совпадения нет — вставляет NULL в столбцы правой таблицы.
💡 Запоминаем: LEFT JOIN = «все из левой + что нашлось в правой». Если не нашлось — NULL. Левая таблица — это та, что после FROM. Правая — та, что после JOIN.

📦 Данные для урока

Используем те же таблицы, что и в прошлом уроке. Для удобства пересоздадим их с дополнительными данными.

CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT
); INSERT INTO customers (id, name, email) VALUES
(1, 'Анна Петрова', 'anna@pizza.ru'),
(2, 'Борис Иванов', 'boris@mail.ru'),
(3, 'Виктория Соколова', 'vika@yandex.ru'),
(4, 'Глеб Новиков', NULL),
(5, 'Дарья Морозова', 'dasha@mail.ru'); -- новый клиент без заказов! CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, product_name TEXT, amount REAL, order_date DATE
); INSERT INTO orders (id, customer_id, product_name, amount, order_date) VALUES
(1, 1, 'Ноутбук', 75000, '2024-02-01'),
(2, 1, 'Мышь', 1500, '2024-02-01'),
(3, 2, 'Клавиатура', 3500, '2024-02-03'),
(4, 3, 'Наушники', 4500, '2024-02-05'),
(5, 1, 'Кружка', 500, '2024-02-07'),
(6, NULL, 'Чайник', 2500, '2024-02-10'); -- заказ без клиента CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL, category_id INTEGER
); INSERT INTO products (id, name, price, category_id) VALUES
(1, 'Ноутбук', 75000, 1),
(2, 'Мышь', 1500, 1),
(3, 'Клавиатура', 3500, 1),
(4, 'Кружка', 500, 2),
(5, 'Тарелка', 300, 2),
(6, 'SQL-бестселлер', 1200, 3),
(7, 'Python для профи', 1500, 3),
(8, 'Наушники', 4500, 1),
(9, 'Чайник', 2500, 2),
(10, 'Стол', 10000, NULL); -- товар без категории CREATE TABLE categories ( id INTEGER PRIMARY KEY, name TEXT NOT NULL
); INSERT INTO categories (id, name) VALUES
(1, 'Электроника'),
(2, 'Посуда'),
(3, 'Книги');

👥 LEFT JOIN на примере: клиенты и заказы

Давай посмотрим на разницу между INNER JOIN и LEFT JOIN наглядно.

-- LEFT JOIN: ВСЕ клиенты + их заказы
SELECT c.id AS customer_id, c.name AS customer, o.id AS order_id, o.product_name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
customer_idcustomerorder_idproduct_nameamount
1Анна Петрова1Ноутбук75 000
1Анна Петрова2Мышь1 500
1Анна Петрова5Кружка500
2Борис Иванов3Клавиатура3 500
3Виктория Соколова4Наушники4 500
4Глеб НовиковNULLNULLNULL
5Дарья МорозоваNULLNULLNULL

Заметил? Глеб Новиков и Дарья Морозова видны! У них нет заказов, но LEFT JOIN всё равно их показывает. В столбцах из таблицы orders — NULL.

А теперь сравни с INNER JOIN:

-- Для сравнения: INNER JOIN — только клиенты с заказами
SELECT c.name, o.id AS order_id, o.product_name, o.amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
-- Результат: 5 строк. Глеб и Дарья НЕ показаны.

INNER JOIN показал бы 5 строк (без Глеба и Дарьи). LEFT JOIN показал 7 строк — все 5 клиентов + те, у кого есть заказы. Глеб и Дарья — с NULL в заказах.

💡 Выбор за тобой: Нужны только клиенты с заказами? INNER JOIN. Нужны ВСЕ клиенты, даже без заказов? LEFT JOIN. Всё зависит от задачи.

🆚 LEFT JOIN vs INNER JOIN — наглядная разница с NULL

Давай сделаем запрос, который наглядно покажет разницу. Мы хотим увидеть клиентов, у которых нет заказов.

-- Клиенты без заказов (LEFT JOIN + WHERE ... IS NULL)
SELECT c.id, c.name AS customer, c.email
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;
idcustomeremail
4Глеб НовиковNULL
5Дарья Морозоваdasha@mail.ru

Это очень распространённый паттерн в SQL. LEFT JOIN + WHERE ... IS NULL — найди строки из левой таблицы, у которых нет пары в правой.

А можно наоборот: найти заказы без клиентов (где customer_id = NULL):

-- Заказы без клиентов
SELECT o.*
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;
idcustomer_idproduct_nameamountorder_date
6NULLЧайник2 5002024-02-10

Заказ №6 — «сирота». Кто его оформил? Неизвестно. NULL в customer_id. LEFT JOIN поможет найти такие «потерянные» записи.

📦 Товары без категории

Помнишь, у нас есть товар «Стол» без категории? Давай найдём все товары и их категории, включая товары без категории.

-- Все товары с категориями (даже без категории)
SELECT p.id, p.name AS product_name, p.price, c.name AS category
FROM products p
LEFT JOIN categories c ON p.category_id = c.id;
idproduct_namepricecategory
1Ноутбук75 000Электроника
2Мышь1 500Электроника
3Клавиатура3 500Электроника
4Кружка500Посуда
5Тарелка300Посуда
6SQL-бестселлер1 200Книги
7Python для профи1 500Книги
8Наушники4 500Электроника
9Чайник2 500Посуда
10Стол10 000NULL

«Стол» теперь виден! В INNER JOIN он бы потерялся. А здесь — мы видим товар, хоть и без категории.

А теперь найдём только товары без категории:

-- Товары без категории
SELECT p.id, p.name, p.price
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE c.id IS NULL;
idnameprice
10Стол10 000

Это очень полезный приём — искать «сирот»: товары без категории, заказы без клиентов, студенты без группы.

💡 Паттерн «Найти сирот»: LEFT JOIN + WHERE + проверка на NULL в столбце правой таблицы (лучше всего проверять первичный ключ — он точно NOT NULL, если строка существует).

📊 LEFT JOIN + GROUP BY + HAVING

Давай теперь сделаем аналитику: посчитаем количество заказов по каждому клиенту, включая тех, у кого заказов 0.

-- Количество заказов по каждому клиенту (включая 0)
SELECT c.id, c.name AS customer, COUNT(o.id) AS "Количество заказов", ROUND(SUM(o.amount), 0) AS "Сумма"
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY "Сумма" DESC;
idcustomerКоличество заказовСумма
1Анна Петрова377 000
3Виктория Соколова14 500
2Борис Иванов13 500
4Глеб Новиков0NULL
5Дарья Морозова0NULL

Глеб и Дарья теперь видны! У них COUNT(o.id) = 0, потому что LEFT JOIN подставил NULL для столбцов из orders, а COUNT(NULL) = 0. Сумма заказов у них NULL — логично, заказов нет.

Можно «причесать» результат с COALESCE:

-- Причешем: NULL → 0
SELECT c.name AS customer, COUNT(o.id) AS orders_count, COALESCE(ROUND(SUM(o.amount), 0), 0) AS total_sum
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY total_sum DESC;
customerorders_counttotal_sum
Анна Петрова377 000
Виктория Соколова14 500
Борис Иванов13 500
Глеб Новиков00
Дарья Морозова00

Гораздо аккуратнее! COALESCE(SUM(amount), 0) превращает NULL в 0.

➡️ RIGHT JOIN (и почему его не используют)

RIGHT JOIN — это то же самое, что LEFT JOIN, но наоборот. Он берёт все строки из правой таблицы.

-- RIGHT JOIN (то же самое, что LEFT JOIN наоборот)
SELECT c.name, o.product_name, o.amount
FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.id;
-- Даст тот же результат, что и LEFT JOIN customers → orders

Почти никто не использует RIGHT JOIN. Почему? Потому что LEFT JOIN читается понятнее. Всегда можно поменять таблицы местами и использовать LEFT JOIN. Это правило хорошего тона в SQL.

💡 Совет: Просто забудь про RIGHT JOIN. Всегда используй LEFT JOIN — просто ставь «главную» таблицу слева (после FROM). Код будет понятнее коллегам.

🔗 Соединение 3 таблиц: LEFT JOIN цепочкой

Соединять можно сколько угодно таблиц. Давай сделаем цепочку: заказы → клиенты → города (добавим таблицу городов):

-- Добавим таблицу городов
CREATE TABLE cities ( id INTEGER PRIMARY KEY, name TEXT NOT NULL
); INSERT INTO cities (id, name) VALUES
(1, 'Москва'),
(2, 'Санкт-Петербург'),
(3, 'Казань'); -- Добавим город клиентам
ALTER TABLE customers ADD COLUMN city_id INTEGER;
UPDATE customers SET city_id = 1 WHERE id IN (1, 4); -- Москва
UPDATE customers SET city_id = 2 WHERE id IN (2, 5); -- СПб
UPDATE customers SET city_id = 3 WHERE id = 3; -- Казань

Теперь соединим все три таблицы:

-- Заказы → клиенты → города (LEFT JOIN на каждом шаге)
SELECT o.id AS order_id, o.product_name, o.amount, c.name AS customer, ct.name AS city
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN cities ct ON c.city_id = ct.id;
order_idproduct_nameamountcustomercity
1Ноутбук75 000Анна ПетроваМосква
2Мышь1 500Анна ПетроваМосква
3Клавиатура3 500Борис ИвановСанкт-Петербург
4Наушники4 500Виктория СоколоваКазань
5Кружка500Анна ПетроваМосква
6Чайник2 500NULLNULL

Мы использовали LEFT JOIN на каждом шаге: сначала соединили с клиентами (чтобы показать заказы даже без клиента), потом с городами (чтобы показать клиентов даже без города).

Важно: если на каком-то шаге получился NULL, дальше он «протаскивается». Клиента нет → город тоже NULL.

🍕 Пример: пиццерия — все клиенты и их заказы

CREATE TABLE pizza_customers ( id INTEGER PRIMARY KEY, name TEXT, phone TEXT
);
INSERT INTO pizza_customers VALUES
(1,'Иван','+7-901-111-11-11'),(2,'Мария','+7-902-222-22-22'),
(3,'Пётр','+7-903-333-33-33'),(4,'Ольга','+7-904-444-44-44'); CREATE TABLE pizza_orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, pizza_name TEXT, price REAL, quantity INTEGER
);
INSERT INTO pizza_orders VALUES
(1,1,'Маргарита',450,2),(2,1,'Пепперони',650,1),
(3,2,'Маргарита',550,3),(4,3,'Гавайская',500,1),
(5,2,'Пепперони',500,2),(6,NULL,'Маргарита',350,1);

Клиент Ольга только зарегистрировалась, но ещё не сделала заказ. LEFT JOIN покажет всех, включая её.

-- Все клиенты и их заказы (даже если заказов нет)
SELECT pc.name AS client, pc.phone, po.id AS order_id, po.pizza_name
FROM pizza_customers pc
LEFT JOIN pizza_orders po ON pc.id = po.customer_id
ORDER BY pc.id;

Ольга появится с NULL в заказах. Без LEFT JOIN мы бы о ней не узнали! Аналитика с LEFT JOIN:

-- Аналитика по клиентам пиццерии
SELECT pc.name AS client, COUNT(po.id) AS "Заказов", COALESCE(SUM(po.price * po.quantity), 0) AS "Потрачено"
FROM pizza_customers pc
LEFT JOIN pizza_orders po ON pc.id = po.customer_id
GROUP BY pc.id, pc.name
ORDER BY "Потрачено" DESC;
clientЗаказовПотрачено
Мария22 650
Иван21 550
Пётр1500
Ольга00

⚠️ Частые ошибки с LEFT JOIN

❌ Ошибка 1: WHERE превращает LEFT JOIN в INNER JOIN

-- Так LEFT JOIN превращается в INNER JOIN!
SELECT c.name, o.product_name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.amount > 1000;
-- ❌ WHERE отфильтровал NULL, Глеб и Дарья пропали! -- Исправление: учитываем NULL
WHERE o.amount > 1000 OR o.amount IS NULL;

❌ Ошибка 2: Неправильный порядок таблиц

-- orders слева — значит, ВСЕ заказы, а не все клиенты!
SELECT c.name, o.product_name
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id;

❌ Ошибка 3: COUNT(*) вместо COUNT(правая.столбец)

-- COUNT(*) считает ВСЕ строки, включая те, что с NULL!
SELECT c.name, COUNT(*) AS cnt
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id;
-- У Глеба COUNT(*) = 1, хотя заказов 0! -- Правильно: COUNT(o.id) — считает только не-NULL
💡 Запоминаем: При LEFT JOIN используй COUNT(столбец_из_правой_таблицы), а не COUNT(*). Иначе клиенты без заказов получат 1 вместо 0.

🧪 Сравнение всех типов JOIN

Давай подведём итог с наглядной таблицей:

Тип JOINЧто попадает в результат
INNER JOIN Только строки с совпадением в обеих таблицах
LEFT JOIN Все строки из левой таблицы + совпадения из правой (NULL, если нет)
RIGHT JOIN Все строки из правой таблицы + совпадения из левой (почти не используется)
FULL OUTER JOIN Все строки из обеих таблиц (есть не во всех СУБД)

Визуально (кружочки Венна):

  • INNER JOIN — пересечение двух кругов. Только то, что есть и там, и там.
  • LEFT JOIN — левый круг целиком + пересечение. Всё из левой + что нашлось в правой.
  • RIGHT JOIN — правый круг целиком + пересечение. Всё из правой + что нашлось в левой.

🏋️ Практические задания

📝 Задание 1. Все клиенты и их заказы

Напиши запрос, который показывает ВСЕХ клиентов (даже без заказов) и их заказы. Выведи имя клиента, название товара и сумму.

👀 Показать ответ
SELECT c.name, o.product_name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

📝 Задание 2. Товары без категории

Найди все товары, у которых нет категории (LEFT JOIN + WHERE IS NULL).

👀 Показать ответ
SELECT p.id, p.name, p.price
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE c.id IS NULL;

📝 Задание 3. Клиенты без заказов

Найди всех клиентов, которые ни разу ничего не заказали.

👀 Показать ответ
SELECT c.*
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;

📝 Задание 4. Количество заказов по каждому клиенту

Посчитай количество заказов для каждого клиента, включая тех, у кого 0 заказов. Отсортируй по убыванию.

👀 Показать ответ
SELECT c.name, COUNT(o.id) AS orders_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY orders_count DESC;

📝 Задание 5. Все товары с категориями, товары без категории — тоже

Выведи все товары с названием категории. Если категории нет — покажи «Без категории».

👀 Показать ответ
SELECT p.name, p.price, COALESCE(c.name, 'Без категории') AS category
FROM products p
LEFT JOIN categories c ON p.category_id = c.id;

📝 Задание 6. Пиццерия: все клиенты с суммой заказов

Используя таблицы pizza_customers и pizza_orders, покажи всех клиентов и их общую сумму заказов. Клиенты без заказов должны быть с суммой 0.

👀 Показать ответ
SELECT pc.name, COALESCE(SUM(po.price * po.quantity), 0) AS total_spent
FROM pizza_customers pc
LEFT JOIN pizza_orders po ON pc.id = po.customer_id
GROUP BY pc.id, pc.name
ORDER BY total_spent DESC;

📋 Шпаргалка: LEFT JOIN

КонструкцияОписание
LEFT JOINВсе строки из левой таблицы + совпадения из правой
LEFT JOIN ... WHERE right IS NULLТолько строки левой таблицы БЕЗ пары в правой
COUNT(правая.столбец)Считает только строки с совпадением (правильно для LEFT JOIN)
COALESCE(SUM(правая.столбец), 0)NULL → 0 для сумм и других агрегатов
LEFT JOIN t2 ON ... LEFT JOIN t3 ON ...Цепочка из трёх и более таблиц

🚀 Что дальше?

Поздравляю! Мы освоили JOIN — один из самых мощных инструментов SQL. Теперь ты умеешь:

  • Соединять таблицы с INNER JOIN (только совпадения)
  • Брать все строки из левой таблицы с LEFT JOIN
  • Находить «сирот» — строки без пары в другой таблице
  • Соединять 3 и больше таблиц в цепочку
  • Считать агрегаты по соединённым данным

В следующих уроках мы узнаем о подзапросах — запросах внутри запросов. Это открывает ещё больше возможностей! А пока — попрактикуйся с JOIN, соединяй таблицы, ищи сирот и строй отчёты. Удачи! 🎯

Урок 4.2 — LEFT JOIN | Модуль 4: JOIN — соединяем таблицы

ТЕСТ — Урок 4.2: LEFT JOIN

5 вопросов

Все пользователи с их заказами

Premium

Поиск пользователей без заказов

Premium