🔗 Урок 4.2: LEFT JOIN
Модуль 4: JOIN — соединяем таблицы
👋 В прошлом уроке мы соединяли таблицы с INNER JOIN — он показывает только то, что есть в обеих таблицах. Но что, если нужно показать все строки из левой таблицы, даже если в правой нет совпадения? Например, всех клиентов, даже тех, кто ничего не заказал. Или все товары, даже без категории. Тут на помощь приходит LEFT JOIN!
🧠 LEFT JOIN — все строки из левой таблицы
LEFT JOIN работает так:
- Берёт ВСЕ строки из левой таблицы (той, что в FROM).
- Для каждой строки ищет совпадение в правой таблице (той, что в JOIN).
- Если совпадение есть — добавляет данные из правой таблицы (как INNER JOIN).
- Если совпадения нет — вставляет NULL в столбцы правой таблицы.
📦 Данные для урока
Используем те же таблицы, что и в прошлом уроке. Для удобства пересоздадим их с дополнительными данными.
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; Заметил? Глеб Новиков и Дарья Морозова видны! У них нет заказов, но 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 в заказах.
🆚 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; Это очень распространённый паттерн в 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; Заказ №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; «Стол» теперь виден! В 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; Это очень полезный приём — искать «сирот»: товары без категории, заказы без клиентов, студенты без группы.
📊 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; Глеб и Дарья теперь видны! У них 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; Гораздо аккуратнее! 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.
🔗 Соединение 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; Мы использовали 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; ⚠️ Частые ошибки с 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 🧪 Сравнение всех типов 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
🚀 Что дальше?
Поздравляю! Мы освоили JOIN — один из самых мощных инструментов SQL. Теперь ты умеешь:
- Соединять таблицы с INNER JOIN (только совпадения)
- Брать все строки из левой таблицы с LEFT JOIN
- Находить «сирот» — строки без пары в другой таблице
- Соединять 3 и больше таблиц в цепочку
- Считать агрегаты по соединённым данным
В следующих уроках мы узнаем о подзапросах — запросах внутри запросов. Это открывает ещё больше возможностей! А пока — попрактикуйся с JOIN, соединяй таблицы, ищи сирот и строй отчёты. Удачи! 🎯
Урок 4.2 — LEFT JOIN | Модуль 4: JOIN — соединяем таблицы
ТЕСТ — Урок 4.2: LEFT JOIN
5 вопросов