Tento článok je momentálne v ruštine — anglický preklad sa pripravuje.
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, а в colors — red, blue, вы получите все 6 пар: S/red, S/blue, M/red и так далее. Количество строк на выходе — это произведение количеств строк во входных таблицах.
Есть и «неявная» форма через запятую — она делает ровно то же самое:
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;
Симптом — неожиданно огромный результат и раздутые суммы в агрегатах: каждая сумма умножается на количество строк во второй таблице. Если запрос внезапно стал считать выручку в десятки раз больше, первое, что стоит проверить, — не размножились ли строки из-за лишнего соединения.
Как защититься:
- Всегда пишите явный
JOIN ... ON вместо соединений через запятую.
- Если вам действительно нужен декартово произведение — пишите
CROSS JOIN явно. Это сигнал ревьюеру, что так и задумано.
- Проверяйте
COUNT(*) до и после добавления join: если число строк скакнуло кратно — почти наверняка случайный кросс-джойн.
- Включите подсветку «декартова произведения» в
EXPLAIN: узел Nested Loop без условия соединения над двумя крупными таблицами — красный флаг.
Gotcha: CROSS JOIN дорог. Произведение растёт мультипликативно, поэтому 100k × 100k строк — это уже 10 миллиардов. Кросс-джойните только маленькие таблицы (дни, категории, размеры), а большие факты добирайте через LEFT JOIN к этому каркасу.
CROSS JOIN — простой инструмент с понятной семантикой: всё со всем. Используйте его осознанно для генерации комбинаций и календарей и держите запятую под подозрением, чтобы он не появился там, где его не звали.
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, а вcolors—red, 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).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;Различия по СУБД:
generate_series. Ряд дат собирают рекурсивным CTE (WITH RECURSIVE) или из таблицы-«цифр».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без условия соединения над двумя крупными таблицами — красный флаг.CROSS JOIN— простой инструмент с понятной семантикой: всё со всем. Используйте его осознанно для генерации комбинаций и календарей и держите запятую под подозрением, чтобы он не появился там, где его не звали.