CROSS JOIN — это соединение таблиц без условия.
Обычный JOIN обычно отвечает на вопрос: «Какие строки из двух таблиц подходят друг другу?»
Например:
- заказ подходит пользователю, если
orders.user_id = users.id;
- товар подходит категории, если
products.category_id = categories.id;
- сотрудник подходит руководителю, если
employees.manager_id = managers.id.
А CROSS JOIN работает иначе. Он не ищет совпадения. Он просто берёт каждую строку из первой таблицы и соединяет её с каждой строкой из второй таблицы.
Так получается декартово произведение.
Если в первой таблице 4 строки, а во второй 7 строк, на выходе будет:
4 * 7 = 28
На слух это может звучать как что-то математическое и редкое. Но на практике CROSS JOIN очень полезен:
- когда нужно получить все комбинации товаров и складов;
- когда нужно построить календарь по дням;
- когда нужно заполнить пропуски в отчёте;
- когда нужно показать нули там, где продаж не было;
- когда нужно собрать сетку «дата × пользователь», «регион × категория», «размер × цвет».
Но у CROSS JOIN есть и опасная сторона. Если он появился случайно, результат может внезапно раздуться в тысячи, миллионы или миллиарды строк.
Разберём спокойно: как он работает, зачем нужен и как не выстрелить себе в ногу.
Что такое декартово произведение
Представим две маленькие таблицы.
Таблица размеров:
S
M
L
Таблица цветов:
red
blue
Если соединить их через CROSS JOIN, получится каждая возможная пара:
S red
S blue
M red
M blue
L red
L blue
Было 3 размера и 2 цвета.
На выходе получилось 6 строк:
3 * 2 = 6
Это и есть декартово произведение: всё со всем.
Синтаксис CROSS JOIN
Синтаксис простой: пишем CROSS JOIN и не пишем ON.
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Здесь нет условия соединения, потому что CROSS JOIN не сопоставляет строки по ключу. Он сразу говорит базе:
Возьми все размеры и соедини каждый размер с каждым цветом.
Если в sizes лежат три строки, а в colors две строки, результатом будет шесть строк.
Почему у CROSS JOIN нет ON
У обычного соединения почти всегда есть условие:
SELECT
u.id,
o.id AS order_id
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
Здесь ON o.user_id = u.id объясняет базе, какие заказы принадлежат каким пользователям.
У CROSS JOIN такого условия нет:
SELECT
u.id,
o.id AS order_id
FROM users AS u
CROSS JOIN orders AS o;
Этот запрос означает:
Соедини каждого пользователя с каждым заказом.
И это почти наверняка не то, что нужно для реального списка заказов пользователей. Если пользователей 1000, а заказов 50000, результат будет:
1000 * 50000 = 50000000
50 миллионов строк.
Поэтому CROSS JOIN нужно использовать осознанно.
Неявный CROSS JOIN через запятую
В SQL есть старая форма записи через запятую:
SELECT
s.size,
c.color
FROM sizes AS s, colors AS c;
Она делает то же самое, что и CROSS JOIN.
То есть этот запрос эквивалентен:
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Проблема в том, что запятая выглядит слишком безобидно. Её легко поставить случайно или забыть условие связи.
Например:
SELECT
u.name,
o.amount
FROM users AS u, orders AS o;
Здесь нет условия, которое связывает пользователя с его заказами. Поэтому база соединит каждого пользователя с каждым заказом.
Правильнее писать явно:
SELECT
u.name,
o.amount
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
А если вам действительно нужно декартово произведение, лучше написать CROSS JOIN прямо. Тогда любой человек на ревью увидит: это не забытое условие, а осознанная генерация комбинаций.
Главное правило количества строк
У CROSS JOIN есть простое правило:
rows = left_rows * right_rows
Если слева 10 строк, а справа 5 строк, получится 50 строк.
Если слева 1000 строк, а справа 365 строк, получится 365000 строк.
Если слева 100000 строк, а справа 100000 строк, получится 10 миллиардов строк.
Вот почему CROSS JOIN отлично подходит для маленьких справочников, но может быть смертельно дорогим на больших таблицах.
Хорошие кандидаты для CROSS JOIN:
- дни месяца;
- категории;
- регионы;
- размеры;
- цвета;
- склады;
- статусы;
- маленькие справочники.
Плохие кандидаты:
- две большие таблицы заказов;
- пользователи × все события;
- товары × все просмотры;
- любые большие фактовые таблицы без фильтра.
Генерация комбинаций: размеры и цвета
Начнём с самого понятного примера.
Есть таблица размеров:
CREATE TABLE sizes (
size text NOT NULL
);
INSERT INTO sizes (size)
VALUES
('S'),
('M'),
('L');
Есть таблица цветов:
CREATE TABLE colors (
color text NOT NULL
);
INSERT INTO colors (color)
VALUES
('red'),
('blue');
Теперь получим все варианты товара:
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c
ORDER BY
s.size,
c.color;
Результат:
L blue
L red
M blue
M red
S blue
S red
В реальной базе вместо размеров и цветов могут быть:
- тарифы и периоды подписки;
- роли и права;
- склады и товары;
- регионы и категории;
- даты и пользователи.
Пример: создать остатки для каждой пары товар-склад
Допустим, в интернет-магазине есть товары и склады.
Таблица products хранит товары, таблица warehouses хранит склады, а таблица inventory хранит остатки.
Иногда нужно заранее создать строку остатка для каждой пары «товар × склад», даже если фактический остаток пока равен нулю.
INSERT INTO inventory (product_id, warehouse_id, qty)
SELECT
p.id,
w.id,
0
FROM products AS p
CROSS JOIN warehouses AS w;
Если товаров 100, а складов 3, запрос создаст 300 строк.
Это как раз хороший сценарий для CROSS JOIN: мы намеренно строим полную матрицу вариантов.
CROSS JOIN как каркас для отчёта
Один из самых полезных сценариев — отчёты с нулями.
Допустим, бизнес хочет видеть выручку по регионам и категориям.
Есть регионы:
north
south
east
west
Есть категории:
books
games
electronics
Даже если в каком-то регионе не было продаж по какой-то категории, строка всё равно должна быть в отчёте. Просто с нулевой выручкой.
Если взять только таблицу заказов, таких строк не будет: нет продаж — нет строк.
Поэтому сначала строят полный каркас через 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 создаёт все пары «регион × категория».
LEFT JOIN пытается найти заказы для каждой пары.
SUM(o.amount) считает выручку.
COALESCE превращает отсутствие продаж в 0.
Без CROSS JOIN в отчёте были бы только те пары, где продажи реально случились.
С CROSS JOIN отчёт становится полным: бизнес видит не только успехи, но и пустые места.
Почему LEFT JOIN здесь важен
Можно спросить: почему после CROSS JOIN используется именно LEFT JOIN, а не обычный JOIN?
Потому что обычный JOIN снова выбросит пары, для которых нет заказов.
А нам как раз нужны все пары.
Вот логика:
regions CROSS JOIN categories
создаёт полный список возможных строк отчёта.
А потом:
LEFT JOIN orders
говорит:
Подтяни заказы, если они есть. Если заказов нет, всё равно оставь строку.
Именно поэтому в отчёте появляются нули.
Календарь по дням без пропусков
Ещё одна классическая задача: отчёт по дням.
Допустим, нужно показать количество заказов за каждый день июня 2026 года.
Если просто сгруппировать заказы по дате, дни без заказов исчезнут:
SELECT
created_at::date AS day,
COUNT(*) AS orders_count
FROM orders
WHERE created_at >= DATE '2026-06-01'
AND created_at < DATE '2026-07-01'
GROUP BY created_at::date
ORDER BY day;
Если 10 июня заказов не было, строки за 10 июня в результате не будет.
Но для графика на фронтенде часто нужен каждый день, даже если значение равно нулю.
В 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;
Здесь generate_series создаёт календарь, а LEFT JOIN подтягивает заказы.
Если в какой-то день заказов не было, строка всё равно останется, а COUNT(o.id) вернёт 0.
Календарь для каждого пользователя
Теперь задача сложнее.
Нужно получить отчёт «день × пользователь»: сколько заказов сделал каждый пользователь в каждый день недели.
Для этого нам нужна плотная сетка:
user 1 + day 1
user 1 + day 2
user 1 + day 3
user 2 + day 1
user 2 + day 2
user 2 + day 3
Вот здесь CROSS JOIN раскрывается полностью.
SELECT
u.id AS user_id,
d::date AS day,
COUNT(o.id) AS orders_count
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;
Что делает запрос:
- Берёт всех пользователей.
- Берёт все дни с 1 по 7 июня.
- Через
CROSS JOIN создаёт каждую пару «пользователь × день».
- Через
LEFT JOIN подтягивает заказы.
- Считает количество заказов.
Если пользователь в какой-то день ничего не заказал, строка всё равно будет. Просто orders_count будет равен 0.
Для аналитики это очень ценно: графики и таблицы не ломаются из-за пропущенных дат.
CROSS JOIN с маленькой таблицей параметров
Иногда CROSS JOIN удобно использовать не с реальной таблицей, а с маленьким набором значений.
Например, нужно посчитать отчёт сразу по нескольким периодам: 7, 30 и 90 дней.
WITH periods AS (
SELECT 7 AS days
UNION ALL
SELECT 30
UNION ALL
SELECT 90
)
SELECT
p.days,
COUNT(o.id) AS orders_count
FROM periods AS p
LEFT JOIN orders AS o
ON o.created_at >= now() - (p.days || ' days')::interval
GROUP BY p.days
ORDER BY p.days;
А если нужно применить эти периоды к каждому пользователю, можно добавить CROSS JOIN.
WITH periods AS (
SELECT 7 AS days
UNION ALL
SELECT 30
UNION ALL
SELECT 90
)
SELECT
u.id AS user_id,
p.days,
COUNT(o.id) AS orders_count
FROM users AS u
CROSS JOIN periods AS p
LEFT JOIN orders AS o
ON o.user_id = u.id
AND o.created_at >= now() - (p.days || ' days')::interval
GROUP BY
u.id,
p.days
ORDER BY
u.id,
p.days;
Получится строка для каждого пользователя и каждого периода.
Случайный CROSS JOIN: как появляется ошибка
Самая неприятная история с CROSS JOIN — когда вы его не планировали.
Например, разработчик пишет запрос:
SELECT
u.name,
o.amount
FROM users AS u, orders AS o;
Он хотел получить заказы пользователей, но забыл условие:
WHERE o.user_id = u.id
В результате база соединит каждого пользователя с каждым заказом.
Если пользователей 1000, а заказов 50000, получится 50 миллионов строк вместо 50000.
Правильный запрос:
SELECT
u.name,
o.amount
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
Поэтому соединения через запятую лучше не использовать в рабочем SQL. Явный JOIN ... ON гораздо безопаснее.
Как случайный CROSS JOIN ломает агрегаты
Особенно коварно случайное декартово произведение в агрегатах.
Допустим, нужно посчитать выручку:
SELECT
SUM(o.amount) AS revenue
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
Если случайно забыть условие соединения:
SELECT
SUM(o.amount) AS revenue
FROM users AS u
CROSS JOIN orders AS o;
каждый заказ повторится для каждого пользователя.
Если пользователей 1000, сумма выручки станет в 1000 раз больше.
Запрос не упадёт с ошибкой. Он просто вернёт красивое, большое и абсолютно неправильное число.
Поэтому если после добавления соединения выручка внезапно выросла в десятки или сотни раз, первым делом проверьте, не размножились ли строки.
Как проверять себя через COUNT
Перед тем как доверять сложному запросу, полезно проверять количество строк на каждом шаге.
Например:
SELECT COUNT(*) AS users_count
FROM users;
SELECT COUNT(*) AS orders_count
FROM orders;
Потом проверяем результат соединения:
SELECT COUNT(*) AS joined_count
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
Если вы ожидали примерно 50000 заказов, а получили 50 миллионов строк, где-то есть размножение.
Для осознанного CROSS JOIN это нормально:
SELECT COUNT(*) AS combinations_count
FROM users AS u
CROSS JOIN generate_series(
DATE '2026-06-01',
DATE '2026-06-07',
INTERVAL '1 day'
) AS d;
Если пользователей 1000, а дней 7, результат должен быть 7000 строк.
То есть COUNT(*) помогает отличить нормальную полную сетку от случайного взрыва данных.
Как увидеть CROSS JOIN в плане выполнения
Если запрос стал подозрительно тяжёлым, можно посмотреть план через EXPLAIN.
EXPLAIN
SELECT
u.name,
o.amount
FROM users AS u
CROSS JOIN orders AS o;
В плане PostgreSQL вы можете увидеть соединение без условия. Часто это будет похоже на Nested Loop, где одна таблица соединяется с другой без нормального условия связи.
Для большой таблицы это красный флаг.
Если вы не планировали декартово произведение, нужно вернуться к запросу и проверить условия JOIN.
Почему CROSS JOIN дорогой
CROSS JOIN растёт не линейно, а мультипликативно.
Добавили немного строк слева и немного строк справа — результат может вырасти очень сильно.
Примеры:
100 * 100 = 10000
1000 * 1000 = 1000000
100000 * 100000 = 10000000000
10 миллиардов строк — это уже не «просто большой результат». Это запрос, который может долго выполняться, активно читать диск, забивать память и мешать другим запросам.
Поэтому правило простое:
Кросс-джойните маленькие наборы данных, а большие таблицы подключайте потом через фильтры и обычные соединения.
Например, хороший подход:
calendar CROSS JOIN small_dimension
LEFT JOIN big_fact_table
Плохой подход:
big_fact_table CROSS JOIN another_big_fact_table
CROSS JOIN в PostgreSQL, MySQL и ClickHouse
В PostgreSQL и MySQL синтаксис CROSS JOIN выглядит одинаково:
SELECT
a.id,
b.id
FROM table_a AS a
CROSS JOIN table_b AS b;
В PostgreSQL для календарей удобно использовать generate_series.
SELECT
d::date AS day
FROM generate_series(
DATE '2026-06-01',
DATE '2026-06-07',
INTERVAL '1 day'
) AS d;
В MySQL нет такой же встроенной функции generate_series. Там даты часто генерируют через рекурсивный CTE или отдельную календарную таблицу.
Пример с рекурсивным CTE:
WITH RECURSIVE dates AS (
SELECT DATE '2026-06-01' AS day
UNION ALL
SELECT day + INTERVAL 1 DAY
FROM dates
WHERE day < DATE '2026-06-07'
)
SELECT day
FROM dates;
В ClickHouse для генерации чисел часто используют numbers, а диапазоны можно разворачивать в строки через функции массивов.
Идея остаётся той же: сначала создаётся набор значений, а потом он соединяется с другими наборами.
Но на больших данных с CROSS JOIN в ClickHouse тоже нужно быть осторожным: правый набор может материализоваться в памяти, и слишком большое декартово произведение быстро станет проблемой.
Когда CROSS JOIN нужен
CROSS JOIN стоит использовать, когда вам действительно нужно получить все сочетания.
Хорошие примеры:
sizes * colors
products * warehouses
dates * users
regions * categories
managers * report_periods
Во всех этих случаях вы строите каркас, где каждая комбинация имеет смысл.
Особенно часто это встречается в аналитике и отчётах:
- показать дни без заказов;
- показать категории без продаж;
- показать склады без остатков;
- показать пользователей без активности;
- подготовить полную матрицу для дальнейшего заполнения фактами.
Когда CROSS JOIN не нужен
CROSS JOIN почти точно не нужен, если между таблицами есть обычная связь.
Например:
- пользователи и заказы;
- заказы и позиции заказа;
- товары и категории;
- сотрудники и отделы;
- платежи и клиенты.
Тут почти всегда должен быть обычный JOIN ... ON.
Например:
SELECT
u.id,
o.id AS order_id
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
А не так:
SELECT
u.id,
o.id AS order_id
FROM users AS u
CROSS JOIN orders AS o;
Если вам хочется написать CROSS JOIN между двумя большими бизнес-таблицами, стоит остановиться и спросить себя: «Мне правда нужны все возможные пары?»
В большинстве случаев ответ будет: нет.
Практический шаблон: полный отчёт с нулями
Вот один из самых полезных шаблонов.
Сначала создаём полный набор комбинаций через CROSS JOIN, потом подтягиваем факты через LEFT JOIN.
SELECT
d.day,
c.id AS category_id,
COALESCE(SUM(o.amount), 0) AS revenue
FROM generate_series(
DATE '2026-06-01',
DATE '2026-06-30',
INTERVAL '1 day'
) AS d(day)
CROSS JOIN categories AS c
LEFT JOIN orders AS o
ON o.category_id = c.id
AND o.created_at::date = d.day
GROUP BY
d.day,
c.id
ORDER BY
d.day,
c.id;
Этот запрос вернёт строку для каждой пары «день × категория».
Если в какой-то день по категории не было заказов, выручка будет равна нулю.
Это именно тот случай, где CROSS JOIN делает отчёт не просто работающим, а аккуратным и полным.
Главное
CROSS JOIN соединяет каждую строку первой таблицы с каждой строкой второй таблицы.
У CROSS JOIN нет условия ON, потому что он не ищет совпадения. Он строит все возможные пары.
Количество строк на выходе равно произведению количества строк во входных наборах.
CROSS JOIN полезен для генерации комбинаций: размеры и цвета, товары и склады, регионы и категории, пользователи и даты.
В отчётах CROSS JOIN часто используют как каркас, а затем через LEFT JOIN подтягивают реальные данные. Так можно показать нули там, где фактов не было.
Для календарей в PostgreSQL удобно использовать generate_series, а потом соединять даты с пользователями, товарами или категориями.
Запись через запятую в FROM может случайно создать декартово произведение. В рабочем SQL лучше писать явные JOIN ... ON и явный CROSS JOIN, если он действительно нужен.
Главная опасность CROSS JOIN — взрыв количества строк. Маленькие справочники соединять удобно, а большие таблицы лучше не кросс-джойнить без очень веской причины.
Главная мысль простая: CROSS JOIN — это инструмент для случаев, когда вам действительно нужно «всё со всем». Если вы используете его осознанно, он помогает строить красивые полные отчёты. Если он появляется случайно, он превращает запрос в машину по размножению строк.
CROSS JOIN— это соединение таблиц без условия.Обычный
JOINобычно отвечает на вопрос: «Какие строки из двух таблиц подходят друг другу?»Например:
orders.user_id = users.id;products.category_id = categories.id;employees.manager_id = managers.id.А
CROSS JOINработает иначе. Он не ищет совпадения. Он просто берёт каждую строку из первой таблицы и соединяет её с каждой строкой из второй таблицы.Так получается декартово произведение.
Если в первой таблице 4 строки, а во второй 7 строк, на выходе будет:
На слух это может звучать как что-то математическое и редкое. Но на практике
CROSS JOINочень полезен:Но у
CROSS JOINесть и опасная сторона. Если он появился случайно, результат может внезапно раздуться в тысячи, миллионы или миллиарды строк.Разберём спокойно: как он работает, зачем нужен и как не выстрелить себе в ногу.
Что такое декартово произведение
Представим две маленькие таблицы.
Таблица размеров:
Таблица цветов:
Если соединить их через
CROSS JOIN, получится каждая возможная пара:Было 3 размера и 2 цвета.
На выходе получилось 6 строк:
Это и есть декартово произведение: всё со всем.
Синтаксис CROSS JOIN
Синтаксис простой: пишем
CROSS JOINи не пишемON.SELECT s.size, c.color FROM sizes AS s CROSS JOIN colors AS c;Здесь нет условия соединения, потому что
CROSS JOINне сопоставляет строки по ключу. Он сразу говорит базе:Если в
sizesлежат три строки, а вcolorsдве строки, результатом будет шесть строк.Почему у CROSS JOIN нет ON
У обычного соединения почти всегда есть условие:
SELECT u.id, o.id AS order_id FROM users AS u JOIN orders AS o ON o.user_id = u.id;Здесь
ON o.user_id = u.idобъясняет базе, какие заказы принадлежат каким пользователям.У
CROSS JOINтакого условия нет:SELECT u.id, o.id AS order_id FROM users AS u CROSS JOIN orders AS o;Этот запрос означает:
И это почти наверняка не то, что нужно для реального списка заказов пользователей. Если пользователей 1000, а заказов 50000, результат будет:
50 миллионов строк.
Поэтому
CROSS JOINнужно использовать осознанно.Неявный CROSS JOIN через запятую
В SQL есть старая форма записи через запятую:
SELECT s.size, c.color FROM sizes AS s, colors AS c;Она делает то же самое, что и
CROSS JOIN.То есть этот запрос эквивалентен:
SELECT s.size, c.color FROM sizes AS s CROSS JOIN colors AS c;Проблема в том, что запятая выглядит слишком безобидно. Её легко поставить случайно или забыть условие связи.
Например:
SELECT u.name, o.amount FROM users AS u, orders AS o;Здесь нет условия, которое связывает пользователя с его заказами. Поэтому база соединит каждого пользователя с каждым заказом.
Правильнее писать явно:
SELECT u.name, o.amount FROM users AS u JOIN orders AS o ON o.user_id = u.id;А если вам действительно нужно декартово произведение, лучше написать
CROSS JOINпрямо. Тогда любой человек на ревью увидит: это не забытое условие, а осознанная генерация комбинаций.Главное правило количества строк
У
CROSS JOINесть простое правило:Если слева 10 строк, а справа 5 строк, получится 50 строк.
Если слева 1000 строк, а справа 365 строк, получится 365000 строк.
Если слева 100000 строк, а справа 100000 строк, получится 10 миллиардов строк.
Вот почему
CROSS JOINотлично подходит для маленьких справочников, но может быть смертельно дорогим на больших таблицах.Хорошие кандидаты для
CROSS JOIN:Плохие кандидаты:
Генерация комбинаций: размеры и цвета
Начнём с самого понятного примера.
Есть таблица размеров:
CREATE TABLE sizes ( size text NOT NULL ); INSERT INTO sizes (size) VALUES ('S'), ('M'), ('L');Есть таблица цветов:
CREATE TABLE colors ( color text NOT NULL ); INSERT INTO colors (color) VALUES ('red'), ('blue');Теперь получим все варианты товара:
SELECT s.size, c.color FROM sizes AS s CROSS JOIN colors AS c ORDER BY s.size, c.color;Результат:
В реальной базе вместо размеров и цветов могут быть:
Пример: создать остатки для каждой пары товар-склад
Допустим, в интернет-магазине есть товары и склады.
Таблица
productsхранит товары, таблицаwarehousesхранит склады, а таблицаinventoryхранит остатки.Иногда нужно заранее создать строку остатка для каждой пары «товар × склад», даже если фактический остаток пока равен нулю.
INSERT INTO inventory (product_id, warehouse_id, qty) SELECT p.id, w.id, 0 FROM products AS p CROSS JOIN warehouses AS w;Если товаров 100, а складов 3, запрос создаст 300 строк.
Это как раз хороший сценарий для
CROSS JOIN: мы намеренно строим полную матрицу вариантов.CROSS JOIN как каркас для отчёта
Один из самых полезных сценариев — отчёты с нулями.
Допустим, бизнес хочет видеть выручку по регионам и категориям.
Есть регионы:
Есть категории:
Даже если в каком-то регионе не было продаж по какой-то категории, строка всё равно должна быть в отчёте. Просто с нулевой выручкой.
Если взять только таблицу заказов, таких строк не будет: нет продаж — нет строк.
Поэтому сначала строят полный каркас через
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создаёт все пары «регион × категория».LEFT JOINпытается найти заказы для каждой пары.SUM(o.amount)считает выручку.COALESCEпревращает отсутствие продаж в0.Без
CROSS JOINв отчёте были бы только те пары, где продажи реально случились.С
CROSS JOINотчёт становится полным: бизнес видит не только успехи, но и пустые места.Почему LEFT JOIN здесь важен
Можно спросить: почему после
CROSS JOINиспользуется именноLEFT JOIN, а не обычныйJOIN?Потому что обычный
JOINснова выбросит пары, для которых нет заказов.А нам как раз нужны все пары.
Вот логика:
создаёт полный список возможных строк отчёта.
А потом:
говорит:
Именно поэтому в отчёте появляются нули.
Календарь по дням без пропусков
Ещё одна классическая задача: отчёт по дням.
Допустим, нужно показать количество заказов за каждый день июня 2026 года.
Если просто сгруппировать заказы по дате, дни без заказов исчезнут:
SELECT created_at::date AS day, COUNT(*) AS orders_count FROM orders WHERE created_at >= DATE '2026-06-01' AND created_at < DATE '2026-07-01' GROUP BY created_at::date ORDER BY day;Если 10 июня заказов не было, строки за 10 июня в результате не будет.
Но для графика на фронтенде часто нужен каждый день, даже если значение равно нулю.
В 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;Здесь
generate_seriesсоздаёт календарь, аLEFT JOINподтягивает заказы.Если в какой-то день заказов не было, строка всё равно останется, а
COUNT(o.id)вернёт0.Календарь для каждого пользователя
Теперь задача сложнее.
Нужно получить отчёт «день × пользователь»: сколько заказов сделал каждый пользователь в каждый день недели.
Для этого нам нужна плотная сетка:
Вот здесь
CROSS JOINраскрывается полностью.SELECT u.id AS user_id, d::date AS day, COUNT(o.id) AS orders_count 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;Что делает запрос:
CROSS JOINсоздаёт каждую пару «пользователь × день».LEFT JOINподтягивает заказы.Если пользователь в какой-то день ничего не заказал, строка всё равно будет. Просто
orders_countбудет равен0.Для аналитики это очень ценно: графики и таблицы не ломаются из-за пропущенных дат.
CROSS JOIN с маленькой таблицей параметров
Иногда
CROSS JOINудобно использовать не с реальной таблицей, а с маленьким набором значений.Например, нужно посчитать отчёт сразу по нескольким периодам: 7, 30 и 90 дней.
WITH periods AS ( SELECT 7 AS days UNION ALL SELECT 30 UNION ALL SELECT 90 ) SELECT p.days, COUNT(o.id) AS orders_count FROM periods AS p LEFT JOIN orders AS o ON o.created_at >= now() - (p.days || ' days')::interval GROUP BY p.days ORDER BY p.days;А если нужно применить эти периоды к каждому пользователю, можно добавить
CROSS JOIN.WITH periods AS ( SELECT 7 AS days UNION ALL SELECT 30 UNION ALL SELECT 90 ) SELECT u.id AS user_id, p.days, COUNT(o.id) AS orders_count FROM users AS u CROSS JOIN periods AS p LEFT JOIN orders AS o ON o.user_id = u.id AND o.created_at >= now() - (p.days || ' days')::interval GROUP BY u.id, p.days ORDER BY u.id, p.days;Получится строка для каждого пользователя и каждого периода.
Случайный CROSS JOIN: как появляется ошибка
Самая неприятная история с
CROSS JOIN— когда вы его не планировали.Например, разработчик пишет запрос:
SELECT u.name, o.amount FROM users AS u, orders AS o;Он хотел получить заказы пользователей, но забыл условие:
WHERE o.user_id = u.idВ результате база соединит каждого пользователя с каждым заказом.
Если пользователей 1000, а заказов 50000, получится 50 миллионов строк вместо 50000.
Правильный запрос:
SELECT u.name, o.amount FROM users AS u JOIN orders AS o ON o.user_id = u.id;Поэтому соединения через запятую лучше не использовать в рабочем SQL. Явный
JOIN ... ONгораздо безопаснее.Как случайный CROSS JOIN ломает агрегаты
Особенно коварно случайное декартово произведение в агрегатах.
Допустим, нужно посчитать выручку:
SELECT SUM(o.amount) AS revenue FROM users AS u JOIN orders AS o ON o.user_id = u.id;Если случайно забыть условие соединения:
SELECT SUM(o.amount) AS revenue FROM users AS u CROSS JOIN orders AS o;каждый заказ повторится для каждого пользователя.
Если пользователей 1000, сумма выручки станет в 1000 раз больше.
Запрос не упадёт с ошибкой. Он просто вернёт красивое, большое и абсолютно неправильное число.
Поэтому если после добавления соединения выручка внезапно выросла в десятки или сотни раз, первым делом проверьте, не размножились ли строки.
Как проверять себя через COUNT
Перед тем как доверять сложному запросу, полезно проверять количество строк на каждом шаге.
Например:
SELECT COUNT(*) AS users_count FROM users;SELECT COUNT(*) AS orders_count FROM orders;Потом проверяем результат соединения:
SELECT COUNT(*) AS joined_count FROM users AS u JOIN orders AS o ON o.user_id = u.id;Если вы ожидали примерно 50000 заказов, а получили 50 миллионов строк, где-то есть размножение.
Для осознанного
CROSS JOINэто нормально:SELECT COUNT(*) AS combinations_count FROM users AS u CROSS JOIN generate_series( DATE '2026-06-01', DATE '2026-06-07', INTERVAL '1 day' ) AS d;Если пользователей 1000, а дней 7, результат должен быть 7000 строк.
То есть
COUNT(*)помогает отличить нормальную полную сетку от случайного взрыва данных.Как увидеть CROSS JOIN в плане выполнения
Если запрос стал подозрительно тяжёлым, можно посмотреть план через
EXPLAIN.EXPLAIN SELECT u.name, o.amount FROM users AS u CROSS JOIN orders AS o;В плане PostgreSQL вы можете увидеть соединение без условия. Часто это будет похоже на
Nested Loop, где одна таблица соединяется с другой без нормального условия связи.Для большой таблицы это красный флаг.
Если вы не планировали декартово произведение, нужно вернуться к запросу и проверить условия
JOIN.Почему CROSS JOIN дорогой
CROSS JOINрастёт не линейно, а мультипликативно.Добавили немного строк слева и немного строк справа — результат может вырасти очень сильно.
Примеры:
10 миллиардов строк — это уже не «просто большой результат». Это запрос, который может долго выполняться, активно читать диск, забивать память и мешать другим запросам.
Поэтому правило простое:
Например, хороший подход:
Плохой подход:
CROSS JOIN в PostgreSQL, MySQL и ClickHouse
В PostgreSQL и MySQL синтаксис
CROSS JOINвыглядит одинаково:SELECT a.id, b.id FROM table_a AS a CROSS JOIN table_b AS b;В PostgreSQL для календарей удобно использовать
generate_series.SELECT d::date AS day FROM generate_series( DATE '2026-06-01', DATE '2026-06-07', INTERVAL '1 day' ) AS d;В MySQL нет такой же встроенной функции
generate_series. Там даты часто генерируют через рекурсивныйCTEили отдельную календарную таблицу.Пример с рекурсивным
CTE:WITH RECURSIVE dates AS ( SELECT DATE '2026-06-01' AS day UNION ALL SELECT day + INTERVAL 1 DAY FROM dates WHERE day < DATE '2026-06-07' ) SELECT day FROM dates;В ClickHouse для генерации чисел часто используют
numbers, а диапазоны можно разворачивать в строки через функции массивов.Идея остаётся той же: сначала создаётся набор значений, а потом он соединяется с другими наборами.
Но на больших данных с
CROSS JOINв ClickHouse тоже нужно быть осторожным: правый набор может материализоваться в памяти, и слишком большое декартово произведение быстро станет проблемой.Когда CROSS JOIN нужен
CROSS JOINстоит использовать, когда вам действительно нужно получить все сочетания.Хорошие примеры:
Во всех этих случаях вы строите каркас, где каждая комбинация имеет смысл.
Особенно часто это встречается в аналитике и отчётах:
Когда CROSS JOIN не нужен
CROSS JOINпочти точно не нужен, если между таблицами есть обычная связь.Например:
Тут почти всегда должен быть обычный
JOIN ... ON.Например:
SELECT u.id, o.id AS order_id FROM users AS u JOIN orders AS o ON o.user_id = u.id;А не так:
SELECT u.id, o.id AS order_id FROM users AS u CROSS JOIN orders AS o;Если вам хочется написать
CROSS JOINмежду двумя большими бизнес-таблицами, стоит остановиться и спросить себя: «Мне правда нужны все возможные пары?»В большинстве случаев ответ будет: нет.
Практический шаблон: полный отчёт с нулями
Вот один из самых полезных шаблонов.
Сначала создаём полный набор комбинаций через
CROSS JOIN, потом подтягиваем факты черезLEFT JOIN.SELECT d.day, c.id AS category_id, COALESCE(SUM(o.amount), 0) AS revenue FROM generate_series( DATE '2026-06-01', DATE '2026-06-30', INTERVAL '1 day' ) AS d(day) CROSS JOIN categories AS c LEFT JOIN orders AS o ON o.category_id = c.id AND o.created_at::date = d.day GROUP BY d.day, c.id ORDER BY d.day, c.id;Этот запрос вернёт строку для каждой пары «день × категория».
Если в какой-то день по категории не было заказов, выручка будет равна нулю.
Это именно тот случай, где
CROSS JOINделает отчёт не просто работающим, а аккуратным и полным.Главное
CROSS JOINсоединяет каждую строку первой таблицы с каждой строкой второй таблицы.У
CROSS JOINнет условияON, потому что он не ищет совпадения. Он строит все возможные пары.Количество строк на выходе равно произведению количества строк во входных наборах.
CROSS JOINполезен для генерации комбинаций: размеры и цвета, товары и склады, регионы и категории, пользователи и даты.В отчётах
CROSS JOINчасто используют как каркас, а затем черезLEFT JOINподтягивают реальные данные. Так можно показать нули там, где фактов не было.Для календарей в PostgreSQL удобно использовать
generate_series, а потом соединять даты с пользователями, товарами или категориями.Запись через запятую в
FROMможет случайно создать декартово произведение. В рабочем SQL лучше писать явныеJOIN ... ONи явныйCROSS JOIN, если он действительно нужен.Главная опасность
CROSS JOIN— взрыв количества строк. Маленькие справочники соединять удобно, а большие таблицы лучше не кросс-джойнить без очень веской причины.Главная мысль простая:
CROSS JOIN— это инструмент для случаев, когда вам действительно нужно «всё со всем». Если вы используете его осознанно, он помогает строить красивые полные отчёты. Если он появляется случайно, он превращает запрос в машину по размножению строк.