В жизни редко обходится одним JOIN’ом. «Покажи имя клиента, дату заказа, название товара и категорию» — это уже четыре таблицы: customers → orders → order_items → products → categories. Этот урок про то, как такие цепочки правильно писать, как они выполняются, и почему в большинстве случаев СУБД сама разберётся, в каком порядке их выполнить.
Цепочка JOIN’ов: как читать
INNER JOIN слева-направо ассоциативен. A JOIN B JOIN C означает «сначала соедини A и B, потом результат соедини с C» — но из-за коммутативности и ассоциативности equi-join’а результат не зависит от порядка. Любой план, который оптимизатор выберет, даст тот же набор строк (если все JOIN’ы — INNER).
Четыре таблицы в одном запросе: клиент → заказ → позиция → товар → категория
Читай это как длинное предложение: «возьми клиентов, добавь к каждому его заказы, к каждому заказу — позиции, к каждой позиции — товар, к каждому товару — категорию». Каждая строка результата — кортеж из пяти таблиц одновременно.
Алиасы — основа читаемости
Без алиасов запрос разваливается:
SELECT customers.full_name, orders.id, order_items.qty, products.name
FROM customers
JOIN orders ON orders.customer_id = customers.id
JOIN order_items ON order_items.order_id = orders.id
JOIN products ON products.id = order_items.product_id;
То же самое с алиасами читается в два раза быстрее. Правила алиасов, которыми пользуется большинство команд:
- Один-два символа от имени таблицы:
customers c,orders o,products p. - Если есть конфликт (два algорifма с похожим префиксом) — добавляй смысловой суффикс:
oi(order_items),pc(parent_category). - Никогда не пиши алиас
t1,t2. Через 2 месяца ты сам не поймёшь, что это. - После присвоения алиаса — обращайся только через него.
customers c, ..., WHERE customers.id = ...— это ошибка, PostgreSQL отвергает.
Ассоциативность INNER JOIN
(A JOIN B) JOIN C и A JOIN (B JOIN C) дают один и тот же результат, если все JOIN’ы — INNER. Это позволяет оптимизатору переставлять таблицы в любом порядке, ища план с минимальной стоимостью.
Например, если в orders 10 миллионов строк, а в customers 100 — оптимизатору выгодно сначала соединить customers с order_items через orders, выкинув ранние ненужные строки. Семантика гарантирует, что результат не изменится.
Любой порядок соединения трёх таблиц даёт идентичный результат. Оптимизатор пользуется этой свободой.
Важно: эта ассоциативность работает только для INNER JOIN. Как только в цепочке появляется OUTER — порядок начинает значить. Об этом — следующий раздел.
OUTER JOIN: порядок имеет значение
(A LEFT JOIN B) LEFT JOIN C ≠ A LEFT JOIN (B LEFT JOIN C) — потенциально это разные запросы. LEFT JOIN не симметричен и не ассоциативен с другими LEFT’ами в общем случае.
В стандартном SQL без скобок цепочка A LEFT JOIN B LEFT JOIN C интерпретируется слева-направо: (A LEFT JOIN B) LEFT JOIN C. Это почти всегда то, что ты имеешь в виду: «бери A, добивай B, добивай C». Каждый последующий LEFT JOIN добавляет новые строки или NULL, но не выкидывает.
Все клиенты + их заказы (если есть) + платежи (если есть). LEFT-цепочка сохраняет сирот на каждом шаге.
В этом запросе мы получаем всех клиентов. Тех, у кого нет заказов — с NULL во второй и третьей таблицах. Тех, у кого есть заказы, но нет платежей — с NULL только в третьей.
Если бы мы написали INNER JOIN payments p вместо LEFT JOIN p, то клиенты без заказов всё равно вернулись бы (благодаря первому LEFT) — но заказы без платежей выпали бы. Тонко.
Когда нужны явные скобки
Скобки в FROM нужны редко, но иногда — необходимы. Типичный пример: смешанные INNER и OUTER JOIN’ы, где ты хочешь сначала жёстко соединить две таблицы, а потом получившийся блок прилепить как OUTER к третьей.
Скобки: сначала INNER (orders + order_items), потом LEFT JOIN к customers
Альтернатива — использовать стандартные скобки прямо в FROM: LEFT JOIN (orders o JOIN order_items oi ON ...) ON .... Это менее распространённый стиль; чаще пишут через производную таблицу (subquery в FROM), как выше — её легче читать.
Комбинаторный взрыв: следи за дубликатами
Главная ловушка многотабличных JOIN’ов — неожиданное умножение строк. Если ты соединил orders с order_items (1 → много), а потом ещё с payments (1 → 1, но…), то одна строка с уровня orders превращается в N × M строк.
Считаем заказы клиента — но получаем НЕ количество заказов, а позиций!
Аня сделала 3 заказа, но COUNT(*) после JOIN с order_items вернёт ~6 (сумма позиций по её заказам). Если хочешь именно число заказов — пиши COUNT(DISTINCT o.id). К этому вернёмся в модуле про агрегаты.
Правило: каждый раз, когда ты добавляешь JOIN с таблицей, где у одного «родителя» может быть несколько детей, проверяй, не превращается ли твой COUNT/SUM в чушь. Это самая частая ошибка в аналитических запросах.
Чек-лист
- INNER JOIN ассоциативен и коммутативен — порядок не влияет на результат, оптимизатор сам выбирает план.
- OUTER JOIN’ы порядок ломают — пиши их слева-направо в логическом порядке наполнения.
- Алиасы — обязательны для запросов с 3+ таблицами. Короткие, осмысленные, без
t1/t2. - Явные скобки в
FROMнужны при смешении INNER/OUTER. Чаще используют производные таблицы. - Главная ловушка: JOIN с дочерней таблицей раздувает строки.
COUNT(*)после такого JOIN’а считает не то, что ты думаешь.