JOIN в SQL: пошаговая инструкция на понятных примерах
За 10 минут вы создадите две таблицы и научитесь связывать их через INNER JOIN, LEFT JOIN и другие варианты. На примерах будет видно, почему строки пропадают и как находить клиентов без заказов.

За 10 минут вы создадите две таблицы в PostgreSQL 16, свяжете клиентов с заказами и получите отчёты без лишних строк. Главное — увидеть, чем отличаются INNER JOIN и LEFT JOIN, почему часть записей исчезает из результата и как проверить запрос до публикации в приложении.
Что нужно подготовить
Инструкция рассчитана на PostgreSQL 16. Подойдёт любой клиент, который умеет отправлять SQL: psql, DBeaver, DataGrip или встроенный редактор облачной базы.
- доступ к тестовой базе, где можно создавать и удалять таблицы;
- PostgreSQL 16 или совместимая версия;
- одна пустая вкладка SQL для выполнения примеров по порядку.
Примеры используют таблицы customers и orders. У Анны и Бориса есть заказы, у Веры заказов нет. Это позволит увидеть разницу между типами соединений.
Шаг 1. Создайте тестовые таблицы
Сначала выполните весь скрипт целиком. Внешний ключ связывает заказ с конкретным клиентом через поле customer_id.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
customer_name TEXT NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
order_total NUMERIC(10, 2) NOT NULL,
order_status TEXT NOT NULL
);
INSERT INTO customers (customer_id, customer_name) VALUES
(1, 'Анна'),
(2, 'Борис'),
(3, 'Вера');
INSERT INTO orders (order_id, customer_id, order_total, order_status) VALUES
(101, 1, 2500.00, 'paid'),
(102, 1, 800.00, 'shipped'),
(103, 2, 1200.00, 'paid');
Проверка: запрос SELECT * FROM customers; должен вернуть три строки, а SELECT * FROM orders; — три заказа. Если таблицы уже существовали, скрипт удалит только эти две таблицы и их данные.
Шаг 2. Соедините строки через INNER JOIN
INNER JOIN возвращает только те строки, для которых нашлась пара в обеих таблицах. В условии после ON указывают, какие поля считать связью.
SELECT
customers.customer_name,
orders.order_id,
orders.order_total
FROM customers
INNER JOIN orders
ON orders.customer_id = customers.customer_id
ORDER BY orders.order_id;
Проверка: результат должен содержать три заказа. В нём будут Анна и Борис, но не Вера: для неё подходящей строки в orders нет.
Псевдонимы делают запрос короче и особенно полезны, когда в обеих таблицах есть столбец с одинаковым именем.
SELECT
c.customer_name,
o.order_id,
o.order_total
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;
В SQL слово JOIN без уточнения обычно означает INNER JOIN. Явный вариант лучше для обучения и ревью: тип соединения сразу виден.
Шаг 3. Сохраните клиентов без заказов через LEFT JOIN
Если отчёт должен показать всех клиентов, начинать нужно с customers и использовать LEFT JOIN. Все строки левой таблицы сохраняются, а для отсутствующей пары справа появляются значения NULL.
SELECT
c.customer_name,
o.order_id,
o.order_total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id, o.order_id;
Проверка: результат должен содержать четыре строки. У Анны будет два заказа, у Бориса — один, у Веры — строка с пустыми значениями order_id и order_total.
Тип JOIN выбирают не по привычке, а по вопросу отчёта: «только пары» — INNER JOIN, «все записи слева, даже без пары» — LEFT JOIN.
Сноска. NULL — специальное значение SQL, которое означает отсутствие известного значения. Это не пустая строка и не ноль: сравнивать его нужно через IS NULL или IS NOT NULL.
Шаг 4. Найдите клиентов без заказов
Комбинация LEFT JOIN и проверки IS NULL превращает соединение в понятный поиск «левых» записей без пары.
SELECT
c.customer_id,
c.customer_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Проверка: запрос должен вернуть только Веру. Не пишите WHERE o.order_id = NULL — такое сравнение не даст ожидаемого результата.
Шаг 5. Посчитайте заказы по каждому клиенту
После соединения можно группировать строки. Для клиента без заказов используйте COUNT(o.order_id), а не COUNT(*): первая форма посчитает только существующие заказы, вторая посчитает сохранённую строку клиента.
SELECT
c.customer_name,
COUNT(o.order_id) AS orders_count,
COALESCE(SUM(o.order_total), 0) AS orders_sum
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
Проверка: у Анны должно быть два заказа на сумму 3300, у Бориса — один на 1200, у Веры — ноль и нулевая сумма. COALESCE заменяет NULL на указанное значение.
Шаг 6. Посмотрите другие виды JOIN
RIGHT JOIN
RIGHT JOIN сохраняет все строки правой таблицы. Тот же результат обычно проще выразить через LEFT JOIN, поменяв таблицы местами:
SELECT
c.customer_name,
o.order_id
FROM orders AS o
RIGHT JOIN customers AS c
ON c.customer_id = o.customer_id;
Проверка: в результате должна остаться Вера, хотя заказа у неё нет. В рабочих запросах чаще используют LEFT JOIN, чтобы направление чтения совпадало с бизнес-вопросом.
FULL OUTER JOIN
FULL OUTER JOIN сохраняет все строки обеих таблиц: и совпавшие, и те, у которых пары нет. В текущих данных все заказы ссылаются на существующих клиентов, поэтому результат будет похож на LEFT JOIN. Разница появится, если в данных есть независимые строки справа.
SELECT
c.customer_name,
o.order_id
FROM customers AS c
FULL OUTER JOIN orders AS o
ON o.customer_id = c.customer_id;
Проверка: запрос должен выполниться в PostgreSQL 16 и вернуть клиентов вместе с заказами. Для поиска непарных строк добавьте условие WHERE c.customer_id IS NULL OR o.order_id IS NULL.
CROSS JOIN
CROSS JOIN создаёт все комбинации строк. Для трёх клиентов и трёх заказов получится девять сочетаний, поэтому без явной задачи такой запрос опасен: объём результата растёт как произведение размеров таблиц.
SELECT
c.customer_name,
o.order_id
FROM customers AS c
CROSS JOIN orders AS o;
Проверка: запрос должен вернуть девять строк. Если вы ожидали три или четыре, нужен не CROSS JOIN, а соединение по ключу.
Что ломается у новичков
Условие соединения написано не в том месте
Связь таблиц задают через ON. Если написать только FROM customers, orders, вы получите декартово произведение — все комбинации строк. Ищите в запросе условие вроде o.customer_id = c.customer_id.
LEFT JOIN превратился в INNER JOIN
Так происходит, когда фильтр по правой таблице ставят в WHERE:
SELECT c.customer_name, o.order_status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.order_status = 'paid';
Строка Веры исчезнет, потому что для неё o.order_status равен NULL. Если нужно сохранить клиентов без заказов и ограничить только присоединяемые заказы, перенесите условие в ON:
SELECT c.customer_name, o.order_status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.order_status = 'paid';
Вместо IS NULL используют знак равенства
Запрос с WHERE o.order_id = NULL не найдёт пропуски. Используйте IS NULL. Для проверки заполненного значения — IS NOT NULL.
Ожидается одна строка, а возвращается несколько
Если у клиента несколько заказов, JOIN создаст по строке на каждый заказ. Это не ошибка соединения. Для одной строки на клиента нужны группировка, агрегатная функция или отдельное правило выбора заказа, например самого нового.
Как проверить JOIN перед использованием в приложении
- Посчитайте строки в каждой исходной таблице.
- Выполните соединение без фильтров и посмотрите, не выросло ли число строк неожиданно.
- Проверьте клиента с несколькими заказами и клиента без заказов.
- Убедитесь, что поля связи действительно относятся к одной сущности и имеют совместимые типы.
- После добавления
WHEREснова проверьте случай сNULL.
Для следующего шага добавьте индекс на поле, по которому часто соединяете большие таблицы, и сравните план запроса через EXPLAIN. Начните с чтения официальной документации PostgreSQL по соединениям, а затем проверьте запрос на данных, где есть дубликаты, пропуски и несколько заказов у одного клиента.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: PostgreSQL 16: Joining Tables, PostgreSQL 16: SELECT
Частые вопросы
Чем INNER JOIN отличается от LEFT JOIN?
Почему после JOIN строк стало больше, чем в исходной таблице?
Почему LEFT JOIN не показывает клиентов без заказов?
Можно ли использовать эти примеры не в PostgreSQL?
Какой JOIN выбрать для отчёта по всем клиентам?
Нужно ли всегда писать полные имена таблиц вместо псевдонимов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.