CROSS JOIN é a única junção que não combina linhas por nenhuma condição. Ela pega cada linha da tabela à esquerda e a emparelha com cada linha da direita. O resultado é um produto cartesiano: 4 linhas à esquerda e 7 à direita geram exatamente 28 linhas de saída. Parece exótico, mas na prática é um cavalo de batalha: gera combinações, preenche lacunas em relatórios e constrói calendários sem falhas. É também a causa mais comum, de longe, de consultas que de repente incham, quando uma junção se infiltra onde ninguém pediu.
O que é um produto cartesiano
A sintaxe é a mais simples possível: não há ON, porque não existe condição de junção:
SELECT s.size, c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Se sizes contém S, M, L e colors contém red, blue, você obtém todos os 6 pares: S/red, S/blue, M/red e assim por diante. O número de linhas de saída é simplesmente o produto do número de linhas de entrada.
Há também uma forma "implícita" com vírgula que faz exatamente a mesma coisa:
SELECT s.size, c.color
FROM sizes AS s, colors AS c;
Essa forma é perigosa: uma vírgula é fácil de digitar por acidente e, então, você acaba com um cross join em vez da junção que pretendia. Por isso, a regra de bolso é sempre escrever CROSS JOIN de forma explícita quando você realmente quer um produto cartesiano.
CROSS JOIN não tem ON e não filtra nada.
- Número de linhas =
count(left) * count(right).
- A sintaxe é idêntica em PostgreSQL, MySQL e ClickHouse. O ClickHouse, porém, reescreve o
CROSS JOIN como um INNER JOIN sem condição e materializa a tabela à direita na memória; com dados grandes, isso pode consumir sua RAM rapidamente.
Gerando combinações
O uso prático mais comum é construir uma "matriz completa" de opções. Digamos que você queira uma linha de estoque para cada par produto-depósito, mesmo onde ainda não há saldo:
INSERT INTO inventory (product_id, warehouse_id, qty)
SELECT p.id, w.id, 0
FROM products AS p
CROSS JOIN warehouses AS w;
Um truque relacionado é calcular uma matriz de métricas para um relatório em que toda combinação precisa aparecer, não apenas as presentes nos dados. Aqui o CROSS JOIN constrói o esqueleto e um LEFT JOIN traz os fatos:
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;
Sem o CROSS JOIN, as regiões sem vendas em uma dada categoria simplesmente sumiriam do relatório. Com ele, uma linha com revenue = 0 aparece de forma explícita, e isso costuma ser exatamente o que o negócio quer.
Calendários e preenchimento de lacunas
Uma tarefa clássica: construir um relatório diário em que toda data esteja presente, mesmo nos dias sem pedidos. No PostgreSQL, generate_series produz a série de datas:
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;
Onde o CROSS JOIN realmente brilha é quando você precisa de um calendário por usuário ou por produto. Faça o cross join da lista de dias com a lista de entidades e você obtém uma grade densa "dia × usuário":
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;
Diferenças entre os motores:
- O MySQL não tem
generate_series. Você constrói uma série de datas com uma CTE recursiva (WITH RECURSIVE) ou com uma tabela de "números/tally".
- O ClickHouse oferece
numbers(N) junto com arrayJoin para expandir um intervalo em linhas.
Cross joins acidentais e como evitá-los
Um cross join é traiçoeiro porque não gera erro: ele silenciosamente retorna linhas demais. A causa habitual é uma vírgula somada a uma condição esquecida no WHERE:
SELECT u.name, o.amount
FROM users u, orders o;
O sintoma é um resultado inesperadamente enorme e agregados inflados: cada soma é multiplicada pelo número de linhas da outra tabela. Se uma consulta de repente reporta uma receita dez vezes maior, a primeira coisa a verificar é se as linhas foram multiplicadas por uma junção perdida.
Como se proteger:
- Escreva sempre
JOIN ... ON de forma explícita em vez de junções com vírgula.
- Se você realmente precisa de um produto cartesiano, escreva
CROSS JOIN explicitamente. Isso avisa ao revisor que a explosão é intencional.
- Verifique o
COUNT(*) antes e depois de adicionar uma junção: se o número de linhas saltou por um múltiplo exato, é quase certo que se trata de um cross join acidental.
- Leia o
EXPLAIN: um Nested Loop sem condição de junção sobre duas tabelas grandes é um sinal de alerta.
Atenção: o CROSS JOIN é caro. O produto cresce de forma multiplicativa, então 100k × 100k linhas já são 10 bilhões. Faça cross join apenas com tabelas pequenas (dias, categorias, tamanhos) e traga os grandes conjuntos de fatos por meio de um LEFT JOIN contra esse esqueleto.
CROSS JOIN é uma ferramenta simples com semântica clara: tudo contra tudo. Recorra a ela de forma deliberada ao gerar combinações e calendários, e mantenha aquela vírgula sob suspeita para que ela nunca apareça sem ser convidada.
CROSS JOINé a única junção que não combina linhas por nenhuma condição. Ela pega cada linha da tabela à esquerda e a emparelha com cada linha da direita. O resultado é um produto cartesiano: 4 linhas à esquerda e 7 à direita geram exatamente 28 linhas de saída. Parece exótico, mas na prática é um cavalo de batalha: gera combinações, preenche lacunas em relatórios e constrói calendários sem falhas. É também a causa mais comum, de longe, de consultas que de repente incham, quando uma junção se infiltra onde ninguém pediu.O que é um produto cartesiano
A sintaxe é a mais simples possível: não há
ON, porque não existe condição de junção:SELECT s.size, c.color FROM sizes AS s CROSS JOIN colors AS c;Se
sizescontémS, M, Lecolorscontémred, blue, você obtém todos os 6 pares:S/red,S/blue,M/rede assim por diante. O número de linhas de saída é simplesmente o produto do número de linhas de entrada.Há também uma forma "implícita" com vírgula que faz exatamente a mesma coisa:
-- equivalent to CROSS JOIN SELECT s.size, c.color FROM sizes AS s, colors AS c;Essa forma é perigosa: uma vírgula é fácil de digitar por acidente e, então, você acaba com um cross join em vez da junção que pretendia. Por isso, a regra de bolso é sempre escrever
CROSS JOINde forma explícita quando você realmente quer um produto cartesiano.CROSS JOINnão temONe não filtra nada.count(left) * count(right).CROSS JOINcomo umINNER JOINsem condição e materializa a tabela à direita na memória; com dados grandes, isso pode consumir sua RAM rapidamente.Gerando combinações
O uso prático mais comum é construir uma "matriz completa" de opções. Digamos que você queira uma linha de estoque para cada par produto-depósito, mesmo onde ainda não há saldo:
INSERT INTO inventory (product_id, warehouse_id, qty) SELECT p.id, w.id, 0 FROM products AS p CROSS JOIN warehouses AS w;Um truque relacionado é calcular uma matriz de métricas para um relatório em que toda combinação precisa aparecer, não apenas as presentes nos dados. Aqui o
CROSS JOINconstrói o esqueleto e umLEFT JOINtraz os fatos: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;Sem o
CROSS JOIN, as regiões sem vendas em uma dada categoria simplesmente sumiriam do relatório. Com ele, uma linha comrevenue = 0aparece de forma explícita, e isso costuma ser exatamente o que o negócio quer.Calendários e preenchimento de lacunas
Uma tarefa clássica: construir um relatório diário em que toda data esteja presente, mesmo nos dias sem pedidos. No PostgreSQL,
generate_seriesproduz a série de datas:-- gap-free calendar for June, with orders per day 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;Onde o
CROSS JOINrealmente brilha é quando você precisa de um calendário por usuário ou por produto. Faça o cross join da lista de dias com a lista de entidades e você obtém uma grade densa "dia × usuário":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;Diferenças entre os motores:
generate_series. Você constrói uma série de datas com uma CTE recursiva (WITH RECURSIVE) ou com uma tabela de "números/tally".numbers(N)junto comarrayJoinpara expandir um intervalo em linhas.Cross joins acidentais e como evitá-los
Um cross join é traiçoeiro porque não gera erro: ele silenciosamente retorna linhas demais. A causa habitual é uma vírgula somada a uma condição esquecida no
WHERE:-- BUG: no join predicate -> Cartesian product SELECT u.name, o.amount FROM users u, orders o; -- forgot WHERE u.id = o.user_idO sintoma é um resultado inesperadamente enorme e agregados inflados: cada soma é multiplicada pelo número de linhas da outra tabela. Se uma consulta de repente reporta uma receita dez vezes maior, a primeira coisa a verificar é se as linhas foram multiplicadas por uma junção perdida.
Como se proteger:
JOIN ... ONde forma explícita em vez de junções com vírgula.CROSS JOINexplicitamente. Isso avisa ao revisor que a explosão é intencional.COUNT(*)antes e depois de adicionar uma junção: se o número de linhas saltou por um múltiplo exato, é quase certo que se trata de um cross join acidental.EXPLAIN: umNested Loopsem condição de junção sobre duas tabelas grandes é um sinal de alerta.CROSS JOINé uma ferramenta simples com semântica clara: tudo contra tudo. Recorra a ela de forma deliberada ao gerar combinações e calendários, e mantenha aquela vírgula sob suspeita para que ela nunca apareça sem ser convidada.