Estos son los dos JOIN que más desconciertan a quien empieza, y por motivos opuestos. El SELF JOIN parece imposible —¿cómo se une una tabla consigo misma sin entrar en un bucle?— y resulta ser la única forma de recorrer las relaciones reflexivas que llevas viendo desde 01-05: empleados.jefe_id y clientes.referido_por_id. El CROSS JOIN parece un error —es el producto cartesiano que en 03-01 presentábamos como accidente— y resulta ser una herramienta deliberada para generar combinaciones completas.

Ninguno de los dos es un "tipo" nuevo de emparejamiento. El SELF JOIN es una técnica que puede usar cualquier tipo (INNER, LEFT...); el CROSS JOIN es el caso degenerado de un JOIN sin condición.

Contenido

  1. SELF JOIN: unir una tabla consigo misma
  2. La jerarquía de empleados
  3. La red de referidos de clientes
  4. SELF JOIN no jerárquico: parejas dentro del mismo grupo
  5. Empleados de la misma ciudad
  6. Jerarquías de profundidad arbitraria
  7. CROSS JOIN: el producto cartesiano deliberado
  8. Cuándo es útil y cuándo es un accidente
  9. CROSS JOIN con generate_series para calendarios
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. SELF JOIN: unir una tabla consigo misma

Un SELF JOIN no tiene sintaxis propia. Es un JOIN normal en el que las dos tablas son la misma:

FROM empleados AS e
JOIN empleados AS jefe ON e.jefe_id = jefe.id

Nada de bucles infinitos: el motor trata cada aparición de la tabla como una relación independiente. Es exactamente el producto cartesiano de 03-01, con empleados a los dos lados, filtrado por la condición del ON.

flowchart LR
    A["empleados<br/>como 'e'<br/>8 filas"] --> C["cartesiano<br/>8 × 8 = 64 filas"]
    B["empleados<br/>como 'jefe'<br/>8 filas"] --> C
    C --> D["filtro ON<br/>e.jefe_id = jefe.id"]
    D --> E["7 filas<br/>(Rosa no tiene jefe)"]

Por qué los alias dejan de ser opcionales

En 03-01 dijimos que los alias de tabla eran una comodidad. En un SELF JOIN son imprescindibles, y por una razón física: sin ellos, el nombre empleados designaría dos cosas distintas a la vez.

-- ⚠️ INCORRECTA
SELECT nombre, jefe_id
FROM empleados
JOIN empleados ON empleados.jefe_id = empleados.id;
ERROR:  table name "empleados" specified more than once

PostgreSQL ni siquiera intenta adivinar: se niega. Con alias distintos, cada aparición tiene identidad propia y todo funciona.

Convención del curso: en un SELF JOIN, los alias no se abrevian por inicial sino que se nombran por el papel que juega cada copia. empleados AS e y empleados AS jefe; clientes AS c y clientes AS referidor; productos AS p1 y productos AS p2 cuando los dos papeles son simétricos. Un alias como e1/e2 es aceptable en parejas simétricas, pero e/jefe es siempre más legible que e1/e2 cuando los papeles son distintos.

  1. La jerarquía de empleados

empleados.jefe_id es una clave foránea que apunta a empleados.id: la relación reflexiva 1:N que dibujaste en el diagrama ER de 01-06. Cada empleado tiene como mucho un jefe, y un jefe puede tener varios subordinados.

2.1. Con INNER JOIN: se pierde la directora

SELECT e.id,
       e.nombre || ' ' || e.apellidos AS empleado,
       e.puesto,
       jefe.nombre || ' ' || jefe.apellidos AS jefe
FROM empleados AS e
JOIN empleados AS jefe ON e.jefe_id = jefe.id
ORDER BY e.id;
id empleado puesto jefe
2 Andrés Company Talens Responsable de ventas Rosa Alcázar Vives
3 Beatriz Nadal Ripoll Responsable de logística Rosa Alcázar Vives
4 Óscar Peris Blasco Comercial Andrés Company Talens
5 Laia Puig Sanchis Comercial Andrés Company Talens
6 Marc Estévez Roig Atención al cliente Andrés Company Talens
7 Irene Salvador Mira Operaria de almacén Beatriz Nadal Ripoll
8 Daniel Vercher Lluch Analista de datos Rosa Alcázar Vives

7 filas de 8. Falta Rosa Alcázar Vives, la directora general, porque su jefe_id es NULL y no casa con nadie. Es exactamente el mecanismo de 03-02: una FK nula nunca encuentra pareja.

Y es un resultado peligroso, porque parece completo. Un organigrama que no incluye a la directora general es un organigrama equivocado.

2.2. Con LEFT JOIN: el organigrama completo

-- ✅ CORRECTA
SELECT e.id,
       e.nombre || ' ' || e.apellidos AS empleado,
       e.puesto,
       jefe.nombre || ' ' || jefe.apellidos AS jefe,
       jefe.puesto AS puesto_jefe
FROM empleados AS e
LEFT JOIN empleados AS jefe ON e.jefe_id = jefe.id
ORDER BY e.id;
id empleado puesto jefe puesto_jefe
1 Rosa Alcázar Vives Directora general (null) (null)
2 Andrés Company Talens Responsable de ventas Rosa Alcázar Vives Directora general
3 Beatriz Nadal Ripoll Responsable de logística Rosa Alcázar Vives Directora general
4 Óscar Peris Blasco Comercial Andrés Company Talens Responsable de ventas
5 Laia Puig Sanchis Comercial Andrés Company Talens Responsable de ventas
6 Marc Estévez Roig Atención al cliente Andrés Company Talens Responsable de ventas
7 Irene Salvador Mira Operaria de almacén Beatriz Nadal Ripoll Responsable de logística
8 Daniel Vercher Lluch Analista de datos Rosa Alcázar Vives Directora general

8 filas: el equipo completo. El NULL de Rosa significa "raíz de la jerarquía", y en un informe se presentaría como "—" o "Dirección" con COALESCE (06-04).

Regla: en un SELF JOIN jerárquico, la raíz del árbol siempre tiene la FK a NULL. Si quieres que aparezca, el JOIN tiene que ser LEFT. Es el error más frecuente al construir organigramas, árboles de categorías o estructuras de carpetas.

2.3. Darle la vuelta: cada jefe con sus subordinados

Cambiando el sentido de la condición se obtiene la relación inversa. Basta con leer el ON al revés:

SELECT jefe.nombre || ' ' || jefe.apellidos AS jefe,
       sub.nombre  || ' ' || sub.apellidos  AS subordinado,
       sub.puesto
FROM empleados AS jefe
JOIN empleados AS sub ON sub.jefe_id = jefe.id
ORDER BY jefe.id, sub.id;

Devuelve las mismas 7 parejas, presentadas desde el punto de vista del jefe. Fíjate en que la condición ON es idéntica; lo único que cambia es qué alias se llama cómo y qué columnas se proyectan. Es la misma lección de 03-04 sobre LEFT y RIGHT: el emparejamiento no cambia, cambia la lectura.

  1. La red de referidos de clientes

clientes.referido_por_id funciona igual, pero con un matiz de negocio distinto: aquí el NULL no significa "raíz de una jerarquía" sino "llegó por su cuenta".

SELECT c.id,
       c.nombre || ' ' || c.apellidos AS cliente,
       c.ciudad,
       ref.nombre || ' ' || ref.apellidos AS referido_por
FROM clientes AS c
LEFT JOIN clientes AS ref ON c.referido_por_id = ref.id
ORDER BY c.id;
id cliente ciudad referido_por
1 Lucía Martínez Soler Valencia (null)
2 Carlos Ferrer Ibáñez Valencia Lucía Martínez Soler
3 Marta Sanchis Gil Castellón Lucía Martínez Soler
4 Javier Ortega Ruiz Madrid (null)
5 Ana Belmonte Roca Barcelona Carlos Ferrer Ibáñez
6 Pau Llorens Vidal Valencia (null)
7 Sofia Moreira Costa Lisboa (null)
8 Tiago Almeida Nunes Oporto Sofia Moreira Costa
9 Camille Dubois Lyon (null)
10 Julien Moreau París Camille Dubois
11 Elena Navarro Puig Alicante Pau Llorens Vidal
12 Diego Ramos Herrera Sevilla (null)
13 Núria Bosch Ferrer Barcelona Ana Belmonte Roca
14 Hugo Iglesias Pardo Zaragoza (null)
15 Inés Carrasco Vega Valencia Lucía Martínez Soler

15 filas: 8 clientes referidos y 7 sin referidor, exactamente los recuentos que 01-06 anunciaba.

La red que dibujan estos datos:

flowchart TD
    L["Lucía (1)"] --> C2["Carlos (2)"]
    L --> M["Marta (3)"]
    L --> I["Inés (15)"]
    C2 --> A["Ana (5)"]
    A --> N["Núria (13)"]
    S["Sofia (7)"] --> T["Tiago (8)"]
    CD["Camille (9)"] --> J["Julien (10)"]
    P["Pau (6)"] --> E["Elena (11)"]
    JA["Javier (4)"]
    D["Diego (12)"]
    H["Hugo (14)"]

Quién trajo a quién: el punto de vista del referidor

Marketing quiere premiar a los clientes que más gente han traído. La consulta parte del referidor:

SELECT ref.nombre || ' ' || ref.apellidos AS referidor,
       c.id AS cliente_id,
       c.nombre || ' ' || c.apellidos AS referido,
       c.fecha_registro
FROM clientes AS ref
JOIN clientes AS c ON c.referido_por_id = ref.id
WHERE ref.id = 1
ORDER BY c.id;
referidor cliente_id referido fecha_registro
Lucía Martínez Soler 2 Carlos Ferrer Ibáñez 2025-01-22
Lucía Martínez Soler 3 Marta Sanchis Gil 2025-02-03
Lucía Martínez Soler 15 Inés Carrasco Vega 2026-01-08

Tres clientes traídos por Lucía, la primera clienta de TiendaVerde. En el módulo 4 contaremos estos referidos por persona con GROUP BY para saber quién encabeza el programa; aquí nos quedamos en el detalle.

Fíjate en un detalle sutil de escritura: aunque el alias ref esté escrito primero en el FROM, sigue siendo la tabla "padre" de la relación. Quién va primero es una decisión de legibilidad, no de significado: lo que fija la dirección es la condición ON c.referido_por_id = ref.id.

  1. SELF JOIN no jerárquico: parejas dentro del mismo grupo

No todo SELF JOIN recorre una relación reflexiva declarada. Otro uso muy frecuente es emparejar filas que comparten un atributo: productos de la misma categoría, empleados de la misma ciudad, pedidos del mismo día.

El equipo de marketing quiere diseñar lotes de dos productos de la misma categoría. Primer intento:

-- ⚠️ INCORRECTA
FROM productos AS p1
JOIN productos AS p2 ON p1.categoria_id = p2.categoria_id

Esto devuelve 78 filas, y tiene dos problemas graves:

Problema Ejemplo
Empareja cada producto consigo mismo (Crema facial, Crema facial): un "lote" de un producto duplicado
Devuelve cada pareja dos veces (Crema, Champú) y (Champú, Crema) son la misma oferta

La solución cabe en tres caracteres: p1.id < p2.id.

-- ✅ CORRECTA
SELECT cat.nombre AS categoria,
       p1.nombre  AS producto_a,
       p1.precio  AS precio_a,
       p2.nombre  AS producto_b,
       p2.precio  AS precio_b
FROM productos  AS p1
JOIN productos  AS p2  ON p1.categoria_id = p2.categoria_id
                      AND p1.id < p2.id
JOIN categorias AS cat ON p1.categoria_id = cat.id
WHERE p1.categoria_id = 2
ORDER BY p1.id, p2.id;
categoria producto_a precio_a producto_b precio_b
Cosmética natural Crema facial de aloe vera 50 ml 18.90 Champú sólido de romero 80 g 8.40
Cosmética natural Crema facial de aloe vera 50 ml 18.90 Aceite corporal de almendras 200 ml 14.25
Cosmética natural Crema facial de aloe vera 50 ml 18.90 Bálsamo labial de caléndula 15 ml 4.60
Cosmética natural Champú sólido de romero 80 g 8.40 Aceite corporal de almendras 200 ml 14.25
Cosmética natural Champú sólido de romero 80 g 8.40 Bálsamo labial de caléndula 15 ml 4.60
Cosmética natural Aceite corporal de almendras 200 ml 14.25 Bálsamo labial de caléndula 15 ml 4.60

6 filas: las 6 parejas posibles entre los 4 productos de Cosmética natural. Sin el WHERE de recorte, la consulta devuelve 29 parejas en todo el catálogo.

Por qué funciona p1.id < p2.id

Este truco merece un párrafo entero, porque es el patrón canónico y se reutiliza en mil sitios.

Para dos productos cualesquiera A y B de la misma categoría, el producto cartesiano genera cuatro combinaciones:

Combinación ¿Cumple p1.id < p2.id? Qué es
(A, A) 5 < 5 es falso Producto consigo mismo
(B, B) Producto consigo mismo
(A, B) con id(A) < id(B) La pareja, una sola vez
(B, A) id(B) < id(A) es falso La misma pareja, duplicada

La condición hace dos trabajos a la vez con un solo operador:

  • < en lugar de <= elimina los pares reflexivos (A, A), porque ningún id es menor que sí mismo.
  • < en lugar de <> elimina el duplicado (B, A), porque de las dos ordenaciones posibles solo una satisface la desigualdad estricta.

La aritmética confirma el efecto: con n productos en una categoría, sin condición hay combinaciones; con <> hay n² − n; con < hay exactamente n(n−1)/2, que es el número de parejas de la combinatoria.

Categoría Productos Sin condición () Con <> Con <
Alimentación 5 25 20 10
Cosmética natural 4 16 12 6
Hogar sostenible 4 16 12 6
Bebidas 4 16 12 6
Higiene personal 2 4 2 1
Complementos 1 1 0 0
Total 20 78 58 29

Nota: este es un JOIN con una condición de desigualdad, lo que en 03-01 llamábamos non-equi join. La igualdad p1.categoria_id = p2.categoria_id define el grupo; la desigualdad p1.id < p2.id selecciona una de las dos ordenaciones. Es habitual que un non-equi join acompañe a uno de igualdad, no que lo sustituya.

  1. Empleados de la misma ciudad

El mismo patrón, aplicado a empleados. RR. HH. quiere organizar comidas de equipo por oficina y necesita las parejas de compañeros que trabajan en la misma ciudad:

SELECT e1.ciudad,
       e1.nombre AS empleado_a,
       e1.puesto AS puesto_a,
       e2.nombre AS empleado_b,
       e2.puesto AS puesto_b
FROM empleados AS e1
JOIN empleados AS e2 ON e1.ciudad = e2.ciudad
                    AND e1.id < e2.id
ORDER BY e1.id, e2.id
LIMIT 10;
ciudad empleado_a puesto_a empleado_b puesto_b
Valencia Rosa Directora general Andrés Responsable de ventas
Valencia Rosa Directora general Beatriz Responsable de logística
Valencia Rosa Directora general Óscar Comercial
Valencia Rosa Directora general Marc Atención al cliente
Valencia Rosa Directora general Irene Operaria de almacén
Valencia Rosa Directora general Daniel Analista de datos
Valencia Andrés Responsable de ventas Beatriz Responsable de logística
Valencia Andrés Responsable de ventas Óscar Comercial
Valencia Andrés Responsable de ventas Marc Atención al cliente
Valencia Andrés Responsable de ventas Irene Operaria de almacén

(10 primeras de 21 filas.)

21 parejas, todas de Valencia: siete de los ocho empleados trabajan allí, y 7 × 6 / 2 = 21. Laia Puig Sanchis no aparece en ninguna fila, porque es la única que trabaja en Castellón y no tiene con quién emparejarse.

Ese detalle es importante: un SELF JOIN de este tipo excluye automáticamente a los elementos únicos de su grupo. Si quisieras un listado en el que Laia también apareciera (con NULL como compañero), harías falta un LEFT JOIN con la misma condición en el ON.

También conviene notar que aquí ciudad admite NULL. Si dos empleados tuvieran la ciudad sin rellenar, NULL = NULL no es verdadero y no se emparejarían, cosa que en este caso es lo correcto: no sabemos si trabajan juntos.

  1. Jerarquías de profundidad arbitraria

Un SELF JOIN recorre exactamente un nivel de la jerarquía. Para subir dos niveles hacen falta dos SELF JOIN encadenados:

SELECT e.id,
       e.nombre AS empleado,
       e.puesto,
       jefe.nombre   AS jefe,
       abuelo.nombre AS jefe_del_jefe
FROM empleados AS e
LEFT JOIN empleados AS jefe   ON e.jefe_id    = jefe.id
LEFT JOIN empleados AS abuelo ON jefe.jefe_id = abuelo.id
ORDER BY e.id;
id empleado puesto jefe jefe_del_jefe
1 Rosa Directora general (null) (null)
2 Andrés Responsable de ventas Rosa (null)
3 Beatriz Responsable de logística Rosa (null)
4 Óscar Comercial Andrés Rosa
5 Laia Comercial Andrés Rosa
6 Marc Atención al cliente Andrés Rosa
7 Irene Operaria de almacén Beatriz Rosa
8 Daniel Analista de datos Rosa (null)

Funciona, y en TiendaVerde es suficiente porque el organigrama tiene solo tres niveles. Pero fíjate en la limitación: el número de niveles está codificado en la consulta. Si la empresa crece a cinco niveles, hay que reescribirla añadiendo dos LEFT JOIN más; si un empleado está a siete niveles de la dirección, esta consulta nunca lo alcanzará.

Para jerarquías de profundidad desconocida hace falta otra herramienta: las CTE recursivas (WITH RECURSIVE), que se estudian en la lección 10-02. Con ellas se recorre un árbol de cualquier profundidad con una sola consulta, y se puede calcular el nivel de cada empleado o la cadena de mando completa. Lo mismo vale para la red de referidos: "todos los clientes que descienden de Lucía, directa o indirectamente" es una consulta recursiva, no un SELF JOIN.

  1. CROSS JOIN: el producto cartesiano deliberado

El CROSS JOIN combina cada fila de una tabla con cada fila de la otra, sin condición alguna. Es el paso 1 del modelo mental de 03-01, sin el paso 2.

FROM categorias AS cat
CROSS JOIN productos AS p

Devuelve 6 × 20 = 120 filas. Ninguna condición, ningún ON: CROSS JOIN no admite ON, y si lo escribes da error de sintaxis.

El tamaño crece como el producto de los cardinales, lo que en tablas reales es explosivo:

Tabla A Tabla B Filas resultantes
categorias (6) proveedores (5) 30
categorias (6) 12 meses 72
productos (20) clientes (15) 300
clientes (15) productos (20) × 12 meses 3 600
lineas_pedido (47) pedidos (20) 940
10 000 10 000 100 000 000
1 000 000 1 000 000 1 000 000 000 000

Esa última fila —un billón de filas— es la razón por la que un CROSS JOIN accidental puede dejar sin memoria a un servidor. La regla es simple: un CROSS JOIN solo es aceptable cuando al menos una de las dos tablas es pequeña y de tamaño conocido.

Las dos formas de escribirlo

-- Forma explícita: recomendada
FROM categorias AS cat CROSS JOIN proveedores AS pr

-- Forma con coma: equivalente, pero indistinguible de un olvido
FROM categorias AS cat, proveedores AS pr

Son idénticas para el motor. La primera es una declaración de intenciones; la segunda es exactamente lo que aparece cuando alguien olvida la condición de emparejamiento con la sintaxis antigua de 03-01.

Convención del curso: si quieres un producto cartesiano, escribe CROSS JOIN en mayúsculas y con un comentario que explique por qué. Es la diferencia entre "esto está pensado" y "esto es un descuido".

  1. Cuándo es útil y cuándo es un accidente

Situación ¿Deliberado? Ejemplo
Generar todas las combinaciones de un informe para que no haya huecos ✅ Sí Categoría × mes, para que un mes sin ventas aparezca con 0
Construir una matriz de variantes ✅ Sí Tallas × colores de un producto textil
Multiplicar una fila de parámetros contra una tabla ✅ Sí Aplicar tres escenarios de IVA a todo el catálogo
Generar datos de prueba en volumen ✅ Sí Cruzar dos series numéricas para crear un millón de filas
Un JOIN al que se le olvidó el ON ❌ No El caso de 03-01: 20 × 15 = 300 filas de basura
Una tabla añadida al FROM sin relacionarla ❌ No Añadir proveedores a una consulta y olvidar unirla

El caso útil por excelencia: informes sin huecos

Este es el motivo por el que el CROSS JOIN existe en el día a día de un analista.

Imagina el informe "ventas por categoría y mes de 2025". Si lo construyes solo con las ventas reales, los meses sin ventas simplemente no aparecen: una categoría que no vendió nada en agosto no tendrá fila de agosto, y el gráfico saltará de julio a septiembre como si agosto no existiera.

La solución profesional consiste en generar primero el esqueleto completo —todas las combinaciones categoría × mes— y unirle después las ventas con un LEFT JOIN:

flowchart LR
    A["categorias<br/>6 filas"] --> C["CROSS JOIN<br/>72 combinaciones"]
    B["12 meses<br/>generate_series"] --> C
    C --> D["LEFT JOIN con las ventas reales"]
    D --> E["informe sin huecos:<br/>los meses sin ventas<br/>aparecen con NULL"]

El esqueleto:

SELECT cat.id AS categoria_id,
       cat.nombre AS categoria,
       m.mes
FROM categorias AS cat
CROSS JOIN generate_series(1, 12) AS m(mes)
ORDER BY cat.id, m.mes;
categoria_id categoria mes
1 Alimentación 1
1 Alimentación 2
1 Alimentación 3
1 Alimentación 4
1 Alimentación 5
1 Alimentación 6
1 Alimentación 7
1 Alimentación 8

(8 primeras de 72 filas.)

72 filas = 6 categorías × 12 meses. Ninguna combinación falta, y sobre esa base el informe queda cuadrado aunque una categoría no venda nada en todo el año. La parte de sumar las ventas es del módulo 4; el esqueleto es de aquí.

La matriz de variantes

El otro caso clásico no necesita ni tablas: se construye con listas literales usando VALUES (que estudiarás a fondo en 05-02).

SELECT talla.t AS talla,
       color.c AS color
FROM (VALUES ('S'), ('M'), ('L'), ('XL')) AS talla(t)
CROSS JOIN (VALUES ('blanco'), ('negro'), ('verde')) AS color(c)
ORDER BY talla.t, color.c;

Devuelve 12 filas: las 4 tallas × 3 colores de un catálogo textil. Es la lista de referencias que habría que dar de alta. TiendaVerde no vende ropa, pero el patrón aparece en cualquier tienda con variantes de producto.

  1. CROSS JOIN con generate_series para calendarios

generate_series es una función generadora de filas de PostgreSQL. Produce una serie de valores y se usa en el FROM como si fuera una tabla:

SELECT * FROM generate_series(1, 5);
generate_series
1
2
3
4
5

Con fechas es donde resulta más útil, porque admite un intervalo como paso:

SELECT m.mes::date
FROM generate_series(DATE '2025-01-01', DATE '2025-12-01', INTERVAL '1 month') AS m(mes);
mes
2025-01-01
2025-02-01
2025-03-01
2025-04-01
2025-05-01
2025-06-01
2025-07-01
2025-08-01
2025-09-01
2025-10-01
2025-11-01
2025-12-01

Y cruzada con categorias genera el calendario completo del informe anterior, ahora con fechas reales:

SELECT cat.nombre AS categoria,
       m.mes::date AS mes
FROM categorias AS cat
CROSS JOIN generate_series(DATE '2025-01-01', DATE '2025-12-01', INTERVAL '1 month') AS m(mes)
ORDER BY cat.id, m.mes;

72 filas, listas para recibir las ventas con un LEFT JOIN.

Nota de dialecto: generate_series es específico de PostgreSQL. Los equivalentes en otros motores:

Motor Cómo generar una serie
PostgreSQL generate_series(inicio, fin [, paso])
SQL Server GENERATE_SERIES (desde 2022); antes, una CTE recursiva o una tabla de calendario
Oracle CONNECT BY LEVEL <= n
MySQL 8+ CTE recursiva WITH RECURSIVE
SQLite CTE recursiva, o la extensión generate_series

La alternativa portable en cualquier motor es mantener una tabla de calendario permanente con una fila por día o por mes. Es lo que hacen la mayoría de los almacenes de datos, y evita depender del dialecto.

LATERAL, de pasada

Existe una variante avanzada del CROSS JOIN en la que la tabla de la derecha puede referirse a columnas de la izquierda:

FROM productos AS p
CROSS JOIN LATERAL (algo que use p.id) AS x

Se llama CROSS JOIN LATERAL (o LEFT JOIN LATERAL) y sirve, por ejemplo, para "las tres reseñas más recientes de cada producto". Necesita subconsultas, que son el módulo 7, así que aquí solo lo mencionamos para que reconozcas la palabra si la ves. LATERAL es estándar SQL y está disponible en PostgreSQL, Oracle y SQL Server (donde se llama CROSS APPLY / OUTER APPLY).

Errores Comunes y Consejos

  • Olvidar los alias en un SELF JOIN. ERROR: table name "empleados" specified more than once. Cada copia necesita su propio nombre.
  • Usar INNER JOIN en una jerarquía. Pierdes la raíz del árbol: Rosa Alcázar Vives desaparece del organigrama porque su jefe_id es NULL. Usa LEFT JOIN.
  • Olvidar la condición p1.id < p2.id al generar parejas. Obtienes cada elemento emparejado consigo mismo y cada pareja por duplicado: 78 filas en vez de 29.
  • Usar <> en lugar de <. Elimina los pares reflexivos pero no los duplicados: 58 filas en vez de 29.
  • Creer que un SELF JOIN recorre toda la jerarquía. Recorre exactamente un nivel. Dos niveles, dos JOIN. Profundidad desconocida, WITH RECURSIVE (10-02).
  • Emparejar por una columna que admite NULL en un SELF JOIN de grupo. Dos empleados con ciudad a NULL no se emparejan, porque NULL = NULL no es verdadero.
  • Escribir un CROSS JOIN con la sintaxis de comas. Es correcto, pero indistinguible de un olvido. Escribe CROSS JOIN explícito.
  • Intentar poner ON en un CROSS JOIN. No lo admite: si necesitas condición, no es un CROSS JOIN, es un INNER JOIN.
  • Cruzar dos tablas grandes "para ver qué sale". Comprueba antes el producto de sus recuentos. 10 000 × 10 000 son cien millones de filas.
  • Consejo: dibuja el árbol antes de escribir el SELF JOIN. Saber quién es padre y quién es hijo evita invertir la condición ON, que es el error más común y el más difícil de ver.
  • Consejo: cuenta las parejas esperadas antes de ejecutar. n(n−1)/2 para las parejas de un grupo de n. Si el resultado no coincide, sabes de inmediato que falta la condición o sobra.
  • Consejo: para informes por periodo, construye siempre primero el esqueleto. CROSS JOIN de dimensiones + LEFT JOIN de los hechos. Es la forma estándar de que no haya huecos, y la usarás en cuanto empieces a agregar en el módulo 4.

Ejercicios

Ejercicio 1

Marketing quiere una ficha del programa de referidos que muestre, para cada cliente: su nombre completo, quién lo refirió (o NULL si llegó por su cuenta) y quién refirió a su referidor (el "abuelo" de la red).

  1. Escribe la consulta usando dos SELF JOIN encadenados sobre clientes.
  2. Restríngela a los clientes que sí tienen referidor y comenta el resultado.
  3. ¿Por qué esta consulta no puede responder a "todos los clientes que descienden de Lucía, a cualquier profundidad"?

Ejercicio 2

Compras quiere lanzar lotes de dos productos de la misma categoría cuyo precio conjunto no supere los 10 €, usando solo productos activos. Escribe la consulta que devuelva la categoría, los dos productos con sus precios y el precio del lote, ordenada por precio del lote.

Explica qué papel juega cada una de las condiciones del ON y del WHERE.

Ejercicio 3

Responde razonando, sin ejecutar:

  1. ¿Cuántas filas devuelve SELECT * FROM empleados AS e1 CROSS JOIN empleados AS e2;?
  2. ¿Y SELECT * FROM empleados AS e1 JOIN empleados AS e2 ON e1.id <> e2.id;?
  3. ¿Y SELECT * FROM empleados AS e1 JOIN empleados AS e2 ON e1.id < e2.id;?
  4. Escribe el esqueleto de un informe proveedor × trimestre de 2025 usando CROSS JOIN y generate_series. ¿Cuántas filas tiene?

Soluciones

Solución 1

1. Con dos SELF JOIN encadenados:

SELECT c.id,
       c.nombre || ' ' || c.apellidos AS cliente,
       ref.nombre  AS referidor,
       ref2.nombre AS referidor_del_referidor
FROM clientes AS c
LEFT JOIN clientes AS ref  ON c.referido_por_id   = ref.id
LEFT JOIN clientes AS ref2 ON ref.referido_por_id = ref2.id
ORDER BY c.id;

Devuelve las 15 filas de clientes, con dos columnas que van quedando a NULL según se agota la cadena.

2. Solo los clientes con referidor (basta cambiar el primer LEFT JOIN por un INNER JOIN; el segundo debe seguir siendo LEFT):

SELECT c.nombre || ' ' || c.apellidos AS cliente,
       ref.nombre  AS referidor,
       ref2.nombre AS referidor_del_referidor
FROM clientes AS c
INNER JOIN clientes AS ref  ON c.referido_por_id   = ref.id
LEFT  JOIN clientes AS ref2 ON ref.referido_por_id = ref2.id
ORDER BY c.id;
cliente referidor referidor_del_referidor
Carlos Ferrer Ibáñez Lucía (null)
Marta Sanchis Gil Lucía (null)
Ana Belmonte Roca Carlos Lucía
Tiago Almeida Nunes Sofia (null)
Julien Moreau Camille (null)
Elena Navarro Puig Pau (null)
Núria Bosch Ferrer Ana Carlos
Inés Carrasco Vega Lucía (null)

8 filas. Solo dos clientes tienen "abuelo" en la red: Ana (traída por Carlos, que a su vez vino de Lucía) y Núria (traída por Ana, que vino de Carlos). El resto desciende directamente de alguien que llegó por su cuenta.

Fíjate en que la cadena Lucía → Carlos → Ana → Núria tiene tres saltos, y esta consulta solo llega a ver dos: en la fila de Núria aparece Carlos como abuelo, pero Lucía —la bisabuela— ya no cabe.

3. Por qué no sirve para "todos los descendientes de Lucía": porque el número de niveles está escrito en la consulta. Cada nivel adicional exige un LEFT JOIN más, y para responder a esa pregunta habría que conocer de antemano la profundidad máxima de la red. Con una cadena de siete referidos, harían falta siete JOIN. La herramienta correcta es una CTE recursiva (WITH RECURSIVE), que recorre el árbol hasta agotarlo con una sola consulta: lección 10-02.

Solución 2

SELECT cat.nombre AS categoria,
       p1.nombre  AS producto_a,
       p1.precio  AS precio_a,
       p2.nombre  AS producto_b,
       p2.precio  AS precio_b,
       ROUND(p1.precio + p2.precio, 2) AS precio_lote
FROM productos  AS p1
JOIN productos  AS p2  ON p1.categoria_id = p2.categoria_id
                      AND p1.id < p2.id
JOIN categorias AS cat ON p1.categoria_id = cat.id
WHERE p1.activo
  AND p2.activo
  AND p1.precio + p2.precio <= 10
ORDER BY p1.precio + p2.precio, p1.id, p2.id;
categoria producto_a precio_a producto_b precio_b precio_lote
Alimentación Pasta de espelta 500 g 2.80 Tomate triturado ecológico 400 g 1.95 4.75
Alimentación Arroz integral ecológico 1 kg 3.90 Tomate triturado ecológico 400 g 1.95 5.85
Alimentación Arroz integral ecológico 1 kg 3.90 Pasta de espelta 500 g 2.80 6.70
Bebidas Infusión de manzanilla ecológica 20 uds 3.25 Kombucha de jengibre 750 ml 4.95 8.20
Bebidas Infusión de manzanilla ecológica 20 uds 3.25 Zumo de naranja prensado en frío 1 L 5.40 8.65

5 lotes posibles, todos de Alimentación y Bebidas: son las dos categorías con productos baratos. En Cosmética natural, el lote más económico sería el bálsamo (4,60 €) con el champú (8,40 €), que ya suma 13 €.

Papel de cada condición:

Condición Dónde Papel
p1.categoria_id = p2.categoria_id ON Emparejamiento: define el grupo dentro del cual se forman parejas
p1.id < p2.id ON Emparejamiento: evita el par reflexivo y el duplicado invertido
p1.categoria_id = cat.id ON Emparejamiento: trae el nombre de la categoría
p1.activo AND p2.activo WHERE Filtrado: descarta productos descatalogados. Excluye las Cápsulas de espirulina, único producto con activo = false
p1.precio + p2.precio <= 10 WHERE Filtrado: la regla de negocio del precio del lote

Las condiciones de emparejamiento van en el ON y las de filtrado en el WHERE, siguiendo la convención de 03-01. Como todos los JOIN son INNER, aquí sería equivalente ponerlas en cualquiera de los dos sitios (03-02), pero la separación mantiene la consulta legible.

Solución 3

1. CROSS JOIN de empleados consigo misma: 8 × 8 = 64 filas. Todas las combinaciones, incluidos los ocho pares de cada empleado consigo mismo.

2. Con ON e1.id <> e2.id: 56 filas. Se eliminan los 8 pares reflexivos (64 − 8), pero cada pareja sigue apareciendo dos veces, en los dos órdenes.

3. Con ON e1.id < e2.id: 28 filas. Es 8 × 7 / 2, el número de parejas distintas de 8 elementos. Cada pareja una sola vez y ninguna consigo misma.

La progresión 64 → 56 → 28 resume toda la sección 4.

4. Esqueleto proveedor × trimestre de 2025:

SELECT pr.id   AS proveedor_id,
       pr.nombre AS proveedor,
       t.trimestre::date AS inicio_trimestre
FROM proveedores AS pr
CROSS JOIN generate_series(DATE '2025-01-01', DATE '2025-10-01', INTERVAL '3 months') AS t(trimestre)
ORDER BY pr.id, t.trimestre;

20 filas = 5 proveedores × 4 trimestres. Los trimestres generados empiezan el 1 de enero, el 1 de abril, el 1 de julio y el 1 de octubre de 2025; el límite superior es 2025-10-01 porque generate_series incluye el extremo final, y poner 2025-12-01 produciría un quinto valor no deseado.

Sobre este esqueleto, un LEFT JOIN con las compras reales daría el informe trimestral por proveedor sin huecos, incluido el proveedor 5 (EcoNordic Supplies, inactivo), que aparecería con todos sus trimestres a NULL.

Conclusión

Cierras los dos JOIN especiales:

  • Un SELF JOIN es un JOIN normal en el que la misma tabla aparece dos veces. Los alias son obligatoriosERROR: table name specified more than once— y conviene nombrarlos por el papel que juegan: e / jefe, c / referidor, p1 / p2.
  • La jerarquía de empleados se recorre con e.jefe_id = jefe.id. Con INNER JOIN se pierde la raíz (7 filas, sin Rosa); con LEFT JOIN aparece el equipo completo (8 filas). En toda jerarquía, la raíz tiene la FK a NULL.
  • La red de referidos de clientes funciona igual: 8 clientes referidos y 7 que llegaron por su cuenta. Lucía Martínez Soler trajo a tres.
  • Para parejas dentro de un mismo grupo —productos de la misma categoría, empleados de la misma ciudad— el patrón canónico es ON a.grupo = b.grupo AND a.id < b.id. El < estricto elimina de un golpe los pares reflexivos y los duplicados invertidos: 29 parejas en vez de 78.
  • Un SELF JOIN recorre un nivel por JOIN. Para profundidad arbitraria hacen falta CTE recursivas (10-02).
  • El CROSS JOIN es el producto cartesiano explícito, sin ON. Es un accidente cuando se olvida la condición de emparejamiento, y una herramienta cuando genera deliberadamente el esqueleto de un informe (categoría × mes) o una matriz de variantes (talla × color).
  • generate_series de PostgreSQL produce series numéricas y de fechas usables en el FROM. Cruzado con una dimensión, da calendarios completos sin huecos; en otros motores se sustituye por CTE recursivas o por una tabla de calendario.

Solo queda una forma de combinar tablas que aún no conoces. Todos los JOIN de este módulo añaden columnas: cogen una fila de aquí, una de allá y las pegan de lado. En la lección siguiente, UNION, INTERSECT y EXCEPT, harás lo contrario: apilar resultados enteros en vertical, añadiendo filas en lugar de columnas. Construirás una lista unificada de contactos a partir de clientes, empleados y proveedores; averiguarás en qué ciudades hay a la vez clientes y empleados; y volverás a encontrar los tres productos nunca vendidos, esta vez sin ningún JOIN.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados