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
SELF JOIN: unir una tabla consigo misma- La jerarquía de
empleados - La red de referidos de
clientes SELF JOINno jerárquico: parejas dentro del mismo grupo- Empleados de la misma ciudad
- Jerarquías de profundidad arbitraria
CROSS JOIN: el producto cartesiano deliberado- Cuándo es útil y cuándo es un accidente
CROSS JOINcongenerate_seriespara calendarios- Errores Comunes y Consejos
- Ejercicios
- Conclusión
SELF JOIN: unir una tabla consigo misma
SELF JOIN: unir una tabla consigo mismaUn SELF JOIN no tiene sintaxis propia. Es un JOIN normal en el que las dos tablas son la misma:
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;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 eyempleados AS jefe;clientes AS cyclientes AS referidor;productos AS p1yproductos AS p2cuando los dos papeles son simétricos. Un alias comoe1/e2es aceptable en parejas simétricas, peroe/jefees siempre más legible quee1/e2cuando los papeles son distintos.
- La jerarquía de
empleados
empleadosempleados.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 JOINjerárquico, la raíz del árbol siempre tiene la FK aNULL. Si quieres que aparezca, elJOINtiene que serLEFT. 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.
- La red de referidos de
clientes
clientesclientes.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.
SELF JOIN no jerárquico: parejas dentro del mismo grupo
SELF JOIN no jerárquico: parejas dentro del mismo grupoNo 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:
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únides 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 n² 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 (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
JOINcon una condición de desigualdad, lo que en 03-01 llamábamos non-equi join. La igualdadp1.categoria_id = p2.categoria_iddefine el grupo; la desigualdadp1.id < p2.idselecciona una de las dos ordenaciones. Es habitual que un non-equi join acompañe a uno de igualdad, no que lo sustituya.
- 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.
- 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 unSELF JOIN.
CROSS JOIN: el producto cartesiano deliberado
CROSS JOIN: el producto cartesiano deliberadoEl 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.
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 prSon 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 JOINen mayúsculas y con un comentario que explique por qué. Es la diferencia entre "esto está pensado" y "esto es un descuido".
- 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.
CROSS JOIN con generate_series para calendarios
CROSS JOIN con generate_series para calendariosgenerate_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:
| 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_serieses 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 calendarioOracle CONNECT BY LEVEL <= nMySQL 8+ CTE recursiva WITH RECURSIVESQLite CTE recursiva, o la extensión generate_seriesLa 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:
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 JOINen una jerarquía. Pierdes la raíz del árbol: Rosa Alcázar Vives desaparece del organigrama porque sujefe_idesNULL. UsaLEFT JOIN. - Olvidar la condición
p1.id < p2.idal 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 JOINrecorre toda la jerarquía. Recorre exactamente un nivel. Dos niveles, dosJOIN. Profundidad desconocida,WITH RECURSIVE(10-02). - Emparejar por una columna que admite
NULLen unSELF JOINde grupo. Dos empleados conciudadaNULLno se emparejan, porqueNULL = NULLno es verdadero. - Escribir un
CROSS JOINcon la sintaxis de comas. Es correcto, pero indistinguible de un olvido. EscribeCROSS JOINexplícito. - Intentar poner
ONen unCROSS JOIN. No lo admite: si necesitas condición, no es unCROSS JOIN, es unINNER 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ónON, 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)/2para las parejas de un grupo den. 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 JOINde dimensiones +LEFT JOINde 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).
- Escribe la consulta usando dos
SELF JOINencadenados sobreclientes. - Restríngela a los clientes que sí tienen referidor y comenta el resultado.
- ¿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:
- ¿Cuántas filas devuelve
SELECT * FROM empleados AS e1 CROSS JOIN empleados AS e2;? - ¿Y
SELECT * FROM empleados AS e1 JOIN empleados AS e2 ON e1.id <> e2.id;? - ¿Y
SELECT * FROM empleados AS e1 JOIN empleados AS e2 ON e1.id < e2.id;? - Escribe el esqueleto de un informe proveedor × trimestre de 2025 usando
CROSS JOINygenerate_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 JOINes unJOINnormal en el que la misma tabla aparece dos veces. Los alias son obligatorios —ERROR: table name specified more than once— y conviene nombrarlos por el papel que juegan:e/jefe,c/referidor,p1/p2. - La jerarquía de
empleadosse recorre cone.jefe_id = jefe.id. ConINNER JOINse pierde la raíz (7 filas, sin Rosa); conLEFT JOINaparece el equipo completo (8 filas). En toda jerarquía, la raíz tiene la FK aNULL. - La red de referidos de
clientesfunciona 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 JOINrecorre un nivel porJOIN. Para profundidad arbitraria hacen falta CTE recursivas (10-02). - El
CROSS JOINes el producto cartesiano explícito, sinON. 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_seriesde PostgreSQL produce series numéricas y de fechas usables en elFROM. 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
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
