sqlpostgresqljoinscross-join

SQL CROSS JOIN: Cartesian Products, Generating Combinations and Calendars

A practical look at CROSS JOIN: Cartesian products, generating every combination and gap-free calendars, plus how to catch accidental cross joins.

3 min czytaniaReferencesql · postgresql · joins · cross-join · analytics
Ten artykuł jest obecnie po rosyjsku — trwa tłumaczenie na angielski.

CROSS JOIN — единственный join, который не сопоставляет строки по условию. Он просто берёт каждую строку левой таблицы и склеивает её с каждой строкой правой. Результат — декартово произведение: если слева 4 строки, а справа 7, на выходе будет ровно 28. Звучит как нечто экзотическое, но на практике это рабочая лошадка для генерации комбинаций, заполнения «дырок» в отчётах и построения непрерывных календарей. А ещё это самый частый источник внезапно распухших запросов, когда join случается там, где его не ждали.

Что такое декартово произведение

Синтаксис максимально простой — никакого ON, потому что условия соединения нет:

SELECT s.size, c.color
FROM sizes  AS s
CROSS JOIN colors AS c;

Если в sizes лежат S, M, L, а в colorsred, blue, вы получите все 6 пар: S/red, S/blue, M/red и так далее. Количество строк на выходе — это произведение количеств строк во входных таблицах.

Есть и «неявная» форма через запятую — она делает ровно то же самое:

-- эквивалентно CROSS JOIN
SELECT s.size, c.color
FROM sizes AS s, colors AS c;

Эта форма опасна: запятую легко поставить случайно, и тогда вы получите кросс-джойн вместо обычного соединения. Поэтому хороший тон — всегда писать CROSS JOIN явно, когда декартово произведение действительно нужно.

  • CROSS JOIN не имеет ON и не фильтрует строки.
  • Число строк = count(left) * count(right).
  • В PostgreSQL, MySQL и ClickHouse синтаксис идентичен. ClickHouse, правда, переписывает CROSS JOIN в INNER JOIN без условия и материализует правую таблицу в память — на больших данных это легко съест RAM.

Генерация комбинаций

Самый частый практический сценарий — построить «полную матрицу» вариантов. Допустим, вы хотите завести запись об остатке для каждой пары товар-склад, даже если фактически остатка пока нет:

INSERT INTO inventory (product_id, warehouse_id, qty)
SELECT p.id, w.id, 0
FROM products  AS p
CROSS JOIN warehouses AS w;

Похожий приём — посчитать матрицу метрик для отчёта, где должны присутствовать все сочетания, а не только те, что встретились в данных. Здесь CROSS JOIN строит каркас, а LEFT JOIN подтягивает факты:

SELECT r.name AS region,
       c.name AS category,
       COALESCE(SUM(o.amount), 0) AS revenue
FROM regions     AS r
CROSS JOIN categories AS c
LEFT JOIN orders AS o
       ON o.region_id   = r.id
      AND o.category_id = c.id
GROUP BY r.name, c.name
ORDER BY r.name, c.name;

Без CROSS JOIN регионы без продаж в какой-то категории просто выпали бы из отчёта. С ним строка revenue = 0 появится явно — а это часто именно то, что нужно бизнесу.

Календари и заполнение пропусков

Классическая задача: построить отчёт по дням, где присутствует каждая дата, даже если заказов в этот день не было. В PostgreSQL для генерации ряда дат есть generate_series:

-- непрерывный календарь за июнь и заказы по дням
SELECT d::date AS day,
       COUNT(o.id) AS orders_count
FROM generate_series(DATE '2026-06-01',
                     DATE '2026-06-30',
                     INTERVAL '1 day') AS d
LEFT JOIN orders AS o
       ON o.created_at::date = d::date
GROUP BY d
ORDER BY d;

А вот где CROSS JOIN раскрывается полностью — когда нужен календарь для каждого пользователя или продукта. Кросс-джойним список дней со списком сущностей и получаем плотную сетку «день × пользователь»:

SELECT u.id AS user_id,
       d::date AS day,
       COUNT(o.id) AS orders
FROM users AS u
CROSS JOIN generate_series(DATE '2026-06-01',
                          DATE '2026-06-07',
                          INTERVAL '1 day') AS d
LEFT JOIN orders AS o
       ON o.user_id = u.id
      AND o.created_at::date = d::date
GROUP BY u.id, d
ORDER BY u.id, day;

Различия по СУБД:

  • В MySQL нет generate_series. Ряд дат собирают рекурсивным CTE (WITH RECURSIVE) или из таблицы-«цифр».
  • В ClickHouse есть функция numbers(N) и arrayJoin для разворачивания диапазона в строки.

Случайные кросс-джойны и как их избежать

Кросс-джойн коварен тем, что не падает с ошибкой — он молча возвращает слишком много строк. Чаще всего его получают случайно из-за запятой и забытого условия в WHERE:

-- БАГ: нет условия связи -> декартово произведение
SELECT u.name, o.amount
FROM users u, orders o;          -- забыли WHERE u.id = o.user_id

Симптом — неожиданно огромный результат и раздутые суммы в агрегатах: каждая сумма умножается на количество строк во второй таблице. Если запрос внезапно стал считать выручку в десятки раз больше, первое, что стоит проверить, — не размножились ли строки из-за лишнего соединения.

Как защититься:

  • Всегда пишите явный JOIN ... ON вместо соединений через запятую.
  • Если вам действительно нужен декартово произведение — пишите CROSS JOIN явно. Это сигнал ревьюеру, что так и задумано.
  • Проверяйте COUNT(*) до и после добавления join: если число строк скакнуло кратно — почти наверняка случайный кросс-джойн.
  • Включите подсветку «декартова произведения» в EXPLAIN: узел Nested Loop без условия соединения над двумя крупными таблицами — красный флаг.

Gotcha: CROSS JOIN дорог. Произведение растёт мультипликативно, поэтому 100k × 100k строк — это уже 10 миллиардов. Кросс-джойните только маленькие таблицы (дни, категории, размеры), а большие факты добирайте через LEFT JOIN к этому каркасу.

CROSS JOIN — простой инструмент с понятной семантикой: всё со всем. Используйте его осознанно для генерации комбинаций и календарей и держите запятую под подозрением, чтобы он не появился там, где его не звали.

Ćwicz na prawdziwych zadaniach

Rozwiązuj zadania w trenerze SQL z natychmiastową oceną i podpowiedziami.

Otwórz trener