SQL и Qt

JOIN: INNER, LEFT, RIGHT, FULL — объединение таблиц

Введение

Связи между таблицами, установленные через внешние ключи (статья 397), реализуются в запросах через JOIN — объединение строк из нескольких таблиц на основе условия соответствия. В статье разберём четыре основных вида JOIN и разницу в том, какие строки каждый из них включает в результат.

Концепция

INNER JOIN включает в результат только те строки, для которых нашлось соответствие в обеих объединяемых таблицах — строки без соответствия с любой стороны полностью исключаются из результата. LEFT JOIN (LEFT OUTER JOIN) включает все строки «левой» таблицы независимо от наличия соответствия в «правой» — для строк без соответствия столбцы правой таблицы заполняются NULL. RIGHT JOIN — зеркальная противоположность LEFT JOIN. FULL JOIN (FULL OUTER JOIN, не поддерживаемый напрямую некоторыми СУБД, в частности MySQL до недавних версий) включает все строки из обеих таблиц независимо от наличия соответствия с любой стороны.

Пример кода: JOIN

-- INNER JOIN — только клиенты, у которых есть хотя бы один заказ
SELECT c.name, o.id AS order_id, o.total_amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;

-- LEFT JOIN — ВСЕ клиенты, включая тех, у кого нет заказов (order_id будет NULL для них)
SELECT c.name, o.id AS order_id, o.total_amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
-- Практичное применение LEFT JOIN: найти клиентов БЕЗ заказов вообще
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL; -- строки, где LEFT JOIN не нашёл соответствия, имеют NULL в полях правой таблицы
-- JOIN нескольких таблиц подряд — типичный паттерн для реальных, многотабличных запросов
SELECT c.name, o.id AS order_id, p.name AS product_name, oi.quantity
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
WHERE o.order_date >= '2026-01-01';
-- FULL JOIN — все строки с обеих сторон (для СУБД без прямой поддержки FULL JOIN
-- эмулируется через UNION LEFT JOIN и RIGHT JOIN, статья 401)
SELECT c.name, o.id AS order_id
FROM customers c
FULL JOIN orders o ON c.id = o.customer_id;

Пояснения к коду

Запрос «клиенты без заказов» показывает классический, важный паттерн использования LEFT JOIN совместно с проверкой WHERE o.id IS NULL — поскольку LEFT JOIN сохраняет все строки левой таблицы (customers) независимо от наличия соответствия, отсутствие соответствия в правой таблице (orders) проявляется как NULL во всех её столбцах для этой строки, и фильтрация именно по NULL в столбце правой таблицы (а не любом столбце левой) выявляет ровно те строки, для которых JOIN не нашёл партнёра. Цепочка из трёх INNER JOIN показывает типичный для реальных приложений многотабличный запрос — соединение клиентов, заказов, позиций заказа и товаров в одном запросе, что значительно эффективнее (и для СУБД, и для разработчика) выполнения нескольких отдельных запросов с последующим соединением результатов на стороне приложения.

Подводные камни

  • Использование INNER JOIN там, где логически нужен LEFT JOIN, что приводит к незаметному «потере» строк, не имеющих соответствия — классическая ошибка: отчёт «все клиенты с суммой их заказов», реализованный через INNER JOIN, незаметно исключит клиентов без единого заказа, что может быть неочевидно при тестировании на данных, где у всех тестовых клиентов есть хотя бы один заказ, но проявится как ошибка в production на реальных данных.
  • Забытое или неверное условие JOIN (ON), приводящее к «декартову произведению» — без условия ON вовсе (что синтаксически допустимо для CROSS JOIN, но крайне редко осмысленно для обычного JOIN), или с неверным, слишком широким условием, результат содержит каждую возможную комбинацию строк обеих таблиц, что для таблиц разумного размера может дать астрономическое количество строк результата и крайне медленное выполнение запроса.
  • Путаница с расположением условия фильтрации в WHERE против включения его прямо в условие ON для LEFT JOIN — для LEFT JOIN эти два варианта НЕ эквивалентны: условие в ON применяется до того, как определяется, есть ли соответствие (и влияет на то, какие строки правой таблицы участвуют в соединении вообще), тогда как условие в WHERE применяется после JOIN, к уже полученному результату (включая строки с NULL от несовпавшего LEFT JOIN), что может неожиданно превратить фактически весь LEFT JOIN обратно в INNER JOIN, если фильтрующее условие в WHERE ссылается на столбец правой таблицы без учёта возможного NULL.
  • Объединение слишком большого количества таблиц в одном запросе без оценки производительности — каждый дополнительный JOIN потенциально увеличивает сложность плана выполнения запроса (статья 416), и для запросов, объединяющих много таблиц, особенно важно иметь правильные индексы (статьи 413-415) на всех столбцах, участвующих в условиях JOIN, иначе производительность может резко деградировать с ростом объёма данных в любой из объединяемых таблиц.