1. Qué es
Dos JOIN que parecen rarezas y no lo son.
CROSS JOIN empareja cada fila con todas las de la otra tabla: el producto cartesiano. Sin ON, porque no hay condición. 8 categorías × 4 regiones = 32 filas.
Self-join es una tabla unida consigo misma. No es un tipo de JOIN distinto —se escribe con JOIN o LEFT JOIN normales— sino un uso: aparece siempre que una fila se relaciona con otra fila de su misma tabla. Jerarquías (reports_to), duplicados, comparaciones entre pares.
Los dos comparten un rasgo: cuando aparecen sin querer, son bugs; cuando aparecen a propósito, resuelven cosas que ninguna otra herramienta resuelve.
2. Sintaxis
-- CROSS JOIN: sin ON
FROM a CROSS JOIN b
-- La forma antigua, idéntica y peligrosa
FROM a, b
-- Self-join: la misma tabla, dos alias distintos
FROM employees e
JOIN employees j ON j.employee_id = e.reports_to
En un self-join los alias no son opcionales: son lo que hace posible la consulta. e y j son la misma tabla vista dos veces, y sin alias no habría forma de decir de cuál hablas.
3. CROSS JOIN
3.1 El producto cartesiano
SELECT count(*) FROM categories CROSS JOIN region; -- 32
SELECT c.category_name, r.region_description
FROM categories c CROSS JOIN region r
ORDER BY 1, 2 LIMIT 5;
category_name | region_description
---------------+--------------------
Beverages | Eastern
Beverages | Northern
Beverages | Southern
Beverages | Western
Condiments | Eastern
8 × 4 = 32. El tamaño se multiplica, y eso es lo que lo hace peligroso: dos tablas de 10.000 filas dan cien millones.
3.2 Cuando aparece sin querer
SELECT count(*) FROM products, categories; -- 616
SELECT count(*) FROM products, categories
WHERE products.category_id = categories.category_id; -- 77
616 contra 77. Con la sintaxis de coma, olvidar la condición no da error: da un producto cartesiano que parece un resultado. Con JOIN ... ON, olvidar el ON da error de sintaxis y te enteras al momento.
Esa es toda la razón por la que la sintaxis de coma se abandonó. No es estilo: es que una de las dos formas te avisa y la otra no.
3.3 Cuando se usa a propósito
El uso legítimo casi siempre es fabricar un esqueleto: todas las combinaciones que deberían existir, para luego comprobar cuáles faltan.
SELECT c.category_id, g.mes
FROM categories c
CROSS JOIN generate_series('1997-01-01'::date, '1997-12-01'::date, '1 month') AS g(mes);
96 filas: 8 categorías × 12 meses. Ninguna viene de los datos — es un calendario completo, inventado a propósito.
¿Para qué? Porque un GROUP BY sobre las ventas solo devuelve los meses que tuvieron ventas (group-by). Si una categoría no vendió nada en marzo, marzo desaparece y el gráfico dibuja una línea continua sobre un agujero. El CROSS JOIN genera el eje completo, y un LEFT JOIN contra las ventas rellena lo que hay. Es el patrón entero de fechas-sin-actividad.
Los otros dos usos reales: plantillas de informe (todas las combinaciones categoría × región aunque estén vacías) y generar datos de prueba.
4. Self-join
4.1 Jerarquías: quién es el jefe de quién
employees.reports_to apunta a employees.employee_id. La tabla se relaciona consigo misma:
SELECT e.first_name || ' ' || e.last_name AS empleado,
j.first_name || ' ' || j.last_name AS jefe
FROM employees e
LEFT JOIN employees j ON j.employee_id = e.reports_to
ORDER BY e.employee_id;
empleado | jefe
------------------+-----------------
Nancy Davolio | Andrew Fuller
Andrew Fuller |
Janet Leverling | Andrew Fuller
Margaret Peacock | Andrew Fuller
Steven Buchanan | Andrew Fuller
Michael Suyama | Steven Buchanan
Robert King | Steven Buchanan
Laura Callahan | Andrew Fuller
Anne Dodsworth | Steven Buchanan
Tiene que ser LEFT JOIN. Andrew Fuller no reporta a nadie (reports_to es NULL), así que con un INNER JOIN desaparecería el director de la lista de empleados — el error más irónico posible.
Y dado la vuelta, cuántos subordinados tiene cada jefe:
SELECT j.first_name || ' ' || j.last_name AS jefe, count(*) AS subordinados
FROM employees e
JOIN employees j ON j.employee_id = e.reports_to
GROUP BY 1 ORDER BY 2 DESC;
jefe | subordinados
-----------------+--------------
Andrew Fuller | 5
Steven Buchanan | 3
Dos niveles: Fuller arriba, Buchanan en medio con 3 a su cargo. Un self-join recorre un solo nivel de jerarquía. Para bajar por un organigrama de profundidad desconocida hace falta una CTE recursiva (cte-recursivas).
4.2 Pares: clientes de la misma ciudad
SELECT a.company_name AS cliente_a, b.company_name AS cliente_b, a.city
FROM customers a
JOIN customers b ON b.city = a.city AND b.customer_id > a.customer_id
ORDER BY a.city LIMIT 5;
cliente_a | cliente_b | city
--------------------------------+------------------------+--------------
Cactus Comidas para llevar | Océano Atlántico Ltda. | Buenos Aires
Cactus Comidas para llevar | Rancho grande | Buenos Aires
Océano Atlántico Ltda. | Rancho grande | Buenos Aires
Furia Bacalhau e Frutos do Mar | Princesa Isabel Vinhos | Lisboa
Around the Horn | B's Beverages | London
El b.customer_id > a.customer_id es la pieza clave. Sin él:
SELECT count(*) FROM customers a JOIN customers b ON b.city = a.city; -- 179
SELECT count(*) FROM customers a JOIN customers b ON b.city = a.city
AND b.customer_id > a.customer_id; -- 44
179 contra 44. Los 179 se descomponen así: 91 filas donde cada cliente se empareja consigo mismo, más 88 = 44 parejas contadas dos veces (A-B y B-A).
| Condición | Qué obtienes |
|---|---|
b.city = a.city |
Cada cliente consigo mismo + cada pareja duplicada |
... AND b.customer_id <> a.customer_id |
Quita los espejos, deja las parejas por duplicado (88) |
... AND b.customer_id > a.customer_id |
44 parejas, cada una una vez |
En un self-join de pares,
>es la condición correcta, no<>. Con<>cada pareja sale dos veces y cualquiercountosumsale al doble.
4.3 Comparar filas de la misma tabla
Productos que cuestan menos de la décima parte que otro de su misma categoría:
SELECT p1.product_name AS caro, p2.product_name AS barato,
p1.unit_price, p2.unit_price
FROM products p1
JOIN products p2 ON p2.category_id = p1.category_id
AND p2.unit_price < p1.unit_price / 10
ORDER BY p1.unit_price DESC LIMIT 3;
caro | barato | unit_price | unit_price
---------------+---------------------------+------------+------------
Côte de Blaye | Laughing Lumberjack Lager | 263.50 | 14.00
Côte de Blaye | Rhönbräu Klosterbier | 263.50 | 7.75
Côte de Blaye | Outback Lager | 263.50 | 15.00
Fíjate en que el ON no lleva ninguna igualdad entre claves: lleva una desigualdad. Es un non-equi join, es legítimo, y es caro: el motor no puede usar los atajos de un emparejamiento por igualdad.
4.4 Duplicados sospechosos
El caso de distinct-y-duplicados, ahora con las dos filas al lado:
SELECT o1.order_id AS pedido_1, o2.order_id AS pedido_2,
o1.customer_id, o1.order_date
FROM orders o1
JOIN orders o2 ON o2.customer_id = o1.customer_id
AND o2.order_date = o1.order_date
AND o2.order_id > o1.order_id
ORDER BY o1.order_id LIMIT 4;
pedido_1 | pedido_2 | customer_id | order_date
----------+----------+-------------+------------
10410 | 10411 | BOTTM | 1997-01-10
10456 | 10457 | KOENE | 1997-02-25
10616 | 10617 | GREAL | 1997-07-31
10713 | 10714 | SAVEA | 1997-10-22
7 parejas. Con GROUP BY ... HAVING count(*) > 1 sabías cuántos había; con el self-join ves los dos ids concretos, que es lo que hace falta para investigar. Y aquí saltan a la vista ids consecutivos —10410/10411, 10456/10457—: dos pedidos creados uno detrás de otro. Eso es exactamente lo que parece un doble clic... y también lo que parece un cliente que pide dos cosas seguidas. El SQL localiza los candidatos; decidir cuáles son errores no es SQL.
5. Errores comunes
FROM a, bsin condición. Producto cartesiano silencioso.- Un
CROSS JOINsobre tablas grandes. 10.000 × 10.000 = cien millones de filas. Se nota. - Self-join con
INNERcuando la referencia puede serNULL. Desaparece el director. - Self-join de pares con
<>en vez de>. Todo sale por duplicado. - Olvidar los alias en un self-join.
column reference is ambiguousen el mejor caso. - Usar self-join para recorrer una jerarquía de más de un nivel. Un self-join = un nivel. Para n niveles,
njoins o una CTE recursiva. - Non-equi joins sobre tablas grandes sin pensar. Correctos y caros.
6. Diferencias con SQL Server
| PostgreSQL | SQL Server | |
|---|---|---|
CROSS JOIN |
✅ | ✅ |
FROM a, b |
✅ (no lo uses) | ✅ (no lo uses) |
| Self-join | ✅ Igual | ✅ Igual |
| Generar series | generate_series(...) |
❌ No existe → tabla de números, master.dbo.spt_values o una CTE recursiva |
Correlación en el FROM |
CROSS JOIN LATERAL |
CROSS APPLY |
generate_series es la diferencia práctica, y aparece cada vez que hay que fabricar un calendario. En SQL Server lo habitual es mantener una tabla de fechas permanente en la base — que, dicho sea de paso, también es buena idea en PostgreSQL cuando los informes de calendario son frecuentes.
CROSS APPLY de SQL Server es el equivalente exacto de CROSS JOIN LATERAL — se ve en lateral.
7. Ejercicios
- Genera todas las combinaciones de categoría y región. ¿Cuántas son?
- Lista cada empleado con su jefe y el cargo del jefe. Los 9 tienen que salir.
- Encuentra las parejas de productos de la misma categoría cuyo precio difiere en menos de 0,50. Cada pareja una sola vez.
- ¿Cuántos empleados hay en cada nivel de la jerarquía: sin jefe, con jefe directo, y con jefe que a su vez tiene jefe?
- Sin ejecutarla, di cuántas filas devuelve y por qué:
sql SELECT count(*) FROM employees e1, employees e2;
8. Pregunta de negocio
El responsable de datos sospecha que hay clientes duplicados: la misma empresa dada de alta dos veces con códigos distintos. Te pide que los busques.
No hay una columna que lo diga. Hay que definir qué significa "el mismo cliente".