Self и cross join
Self-join соединяет таблицу с собой, чтобы обходить иерархии или сравнивать строки попарно; дедуп пар через a.id < b.id. Явный CROSS JOIN строит осознанные произведения — календарные сетки, заполнение пропусков, все комбинации — в отличие от случайного join через запятую.
Запрос «найти дубли товаров» соединил таблицу products с собой, сматчил по равному имени и вернул 1,4 миллиона пар из таблицы с 1200 дублями. Каждый дубль сообщался дважды (A–B и B–A), и каждая строка совпадала ещё и сама с собой (A–A). Логика была верна; дедуп отсутствовал. Self-join — самый ошибкоопасный join именно потому, что обе стороны — одна таблица — и фикс это один оператор сравнения, который большинство забывает.
Self-join: таблица, соединённая с собой
Self-join — это просто join, где обе стороны — одна и та же таблица под двумя разными алиасами. В движке нет ничего особого — та же машинерия отфильтрованного произведения — но он открывает два целых класса запросов.
Обход иерархии (adjacency list). Частый паттерн хранит дерево так, что каждая строка указывает на родителя: у users может быть колонка referred_by, а таблица в стиле employees имеет manager_id, ссылающийся на id той же таблицы. Чтобы спарить каждого сотрудника с именем его менеджера, соедини таблицу с собой:
-- Каждый сотрудник рядом с именем своего менеджера (один уровень иерархии)
SELECT e.id, e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
-- LEFT JOIN, чтобы CEO (manager_id IS NULL) тоже появился, manager = NULLLEFT JOIN важен: у вершины дерева нет родителя, поэтому inner self-join молча выбросил бы корень. (Один self-join поднимается ровно на один уровень; предки произвольной глубины требуют рекурсивного CTE — раздел 05.)
Попарное сравнение. Другое применение — сравнение строк таблицы с другими строками той же таблицы: найти дубли товаров, пользователей из одного города, заказы в пределах часа друг от друга. Здесь и живёт баг из хука.
-- Пары товаров с одинаковым именем — дедуплены и без самосовпадений
SELECT a.id AS id_a, b.id AS id_b, a.name
FROM products a
JOIN products b ON a.name = b.name
WHERE a.id < b.id; -- критическая строкаБез WHERE a.id < b.id ты получаешь три вида мусора: каждая строка совпадает сама с собой (a.id = b.id), и каждая настоящая пара появляется дважды в обоих порядках (A–B и B–A). Предикат a.id < b.id решает оба разом — он исключает самосовпадения (id никогда не меньше себя) и оставляет лишь один порядок каждой пары. Используй < для неупорядоченных пар; используй <> лишь если тебе действительно нужны оба направления.
Cross join: осознанное произведение
Прошлый урок подал декартово произведение как грабли. Но иногда произведение — ровно тот результат, что нужен, и ты пишешь CROSS JOIN, чтобы сказать это намеренно. Канонический случай — плотная сетка — каждая комбинация двух измерений, включая пустые ячейки.
-- Каждая ячейка (день, товар) для 7-дневного отчёта, даже дни с нулём продаж
SELECT d.day, p.id AS product_id, COALESCE(s.qty, 0) AS qty
FROM generate_series(DATE '2026-06-01', DATE '2026-06-07', INTERVAL '1 day') AS d(day)
CROSS JOIN products p
LEFT JOIN sales s ON s.product_id = p.id AND s.sold_on = d.day;Здесь generate_series производит 7 дней, CROSS JOIN products делает каждую комбинацию день-товар (каркас), а LEFT JOIN к реальным продажам заполняет количества, дефолтя в 0 для пустых ячеек. Без cross join дни без продаж были бы вовсе отсутствующими строками — классический пробел в отчёте. Другие осознанные применения: матрица вариантов «все размеры × все цвета» или спаривание каждой строки с фиксированным набором корзин.
Различие, на которое указывает заголовок урока: явный CROSS JOIN читается как намерение — «да, мне нужно произведение». Произведение, прокравшееся через join по запятой (FROM a, b без WHERE, связывающего их) или слишком слабый ON, — это случайность и авария из урока 1. Пиши CROSS JOIN, когда имеешь это в виду; никогда не позволяй произведению случиться молча.
▸Почему это работает
Зачем явное ключевое слово CROSS JOIN вместо старого синтаксиса с запятой? Читаемость и безопасность. FROM a, b, c WHERE … скрывает, намеренно ли произведение; ревьюер не отличит осознанную сетку от забытого условия join. a CROSS JOIN b заявляет, что произведение нарочно, а резервирование форм запятая/JOIN для связанных таблиц делает пропущенный предикат визуально очевидным. Многие команды вовсе линтят запятые-join ровно по этой причине — синтаксис, делающий случайные произведения лёгкими, и есть тот, который стоит запретить.
Self-join находит пары пользователей из одного города. Зачем добавлять WHERE a.id < b.id?
Ты строишь ежедневный отчёт и хочешь, чтобы каждая ячейка (день, товар) появлялась, включая дни с нулём продаж. Какая структура гарантирует существование пустых ячеек?
Заполни пропуск: явный CROSS JOIN говорит, что декартово произведение _______ — написано нарочно ради построения сетки — тогда как произведение, прокравшееся через join по запятой без связывающего условия, это случайность.
- 01Какие две проблемы решает WHERE a.id < b.id в попарном self-join и что было бы без него?
- 02Почему использовать LEFT JOIN (а не INNER) при self-join сотрудников к их менеджерам и чего один self-join НЕ делает?
- 03Приведи законное применение явного CROSS JOIN и объясни, почему он бьёт inner join здесь.
Self-join соединяет таблицу с собой под двумя алиасами, переиспользуя обычную машинерию отфильтрованного произведения для двух задач. Для иерархий (adjacency list вроде employees.manager_id → id) соедини таблицу с собой через LEFT JOIN, чтобы корень — чей родитель NULL — выжил; помни, что один self-join поднимается лишь на один уровень, а произвольная глубина требует рекурсивного CTE (раздел 05). Для попарного сравнения (дубли, пользователи одного города) незаменима строка WHERE a.id < b.id: она убирает самосовпадения и зеркальный дубль каждой пары одним махом. CROSS JOIN — это осознанное декартово произведение, законный близнец случайного из урока 1: generate_series(...) CROSS JOIN products LEFT JOIN sales строит плотную сетку отчёта, где появляются даже дни с нулём продаж, — матрицу всех комбинаций, которую одни данные дать не могут. Пиши CROSS JOIN, чтобы сигналить намерение, и держи join по запятой вне кодовой базы, чтобы забытый предикат никогда не притворился сеткой. Теперь, когда пишешь попарный self-join, потянешься за WHERE a.id < b.id ещё до запуска — без него каждая реальная пара тихо появится дважды, а каждая строка совпадёт сама с собой.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.