sqlpostgresqljoinscross-join

SQL CROSS JOIN: produtos cartesianos, geração de combinações e calendários

Um olhar prático sobre o CROSS JOIN: produtos cartesianos, geração de todas as combinações e calendários sem lacunas, além de como detectar cross joins acidentais.

4 min de leituraReferênciasql · postgresql · joins · cross-join · analytics

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:

-- 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 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:

-- 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 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:

-- BUG: no join predicate -> Cartesian product
SELECT u.name, o.amount
FROM users u, orders o;          -- forgot WHERE u.id = o.user_id

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.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador