Cerraste el módulo 2 chocando siempre contra el mismo techo: pedidos te dice cliente_id = 9 y no "Camille Dubois", lineas_pedido te dice producto_id = 15 y no "Té verde matcha ceremonial". Ese techo se acaba aquí. En esta lección aprenderás qué es realmente un JOIN, no como una fórmula que se copia sino como una operación que puedes reconstruir mentalmente paso a paso: un producto cartesiano filtrado por una condición. A partir de esa idea, todo lo demás —INNER, LEFT, RIGHT, FULL, SELF, CROSS— deja de ser una lista de nombres que memorizar y se convierte en variaciones sobre un mismo mecanismo.

Esta es la lección paraguas del módulo: te da el marco conceptual, la sintaxis y la panorámica de los cinco tipos. Cada uno de ellos se desarrolla a fondo en las lecciones siguientes.

Contenido

  1. Por qué hay que recomponer lo que la normalización separó
  2. Qué es un JOIN: producto cartesiano filtrado
  3. La cláusula ON y la condición de emparejamiento
  4. Sintaxis moderna frente a la antigua de comas
  5. El CROSS JOIN accidental
  6. USING y NATURAL JOIN
  7. Alias de tabla y la ambigüedad de nombres
  8. Panorámica de los cinco tipos de JOIN
  9. Encadenar tres o más tablas
  10. Los JOIN en el orden lógico de ejecución
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Por qué hay que recomponer lo que la normalización separó

En la lección 01-05 justificamos por qué el nombre de la categoría no se guarda dentro de cada producto: si "Cosmética natural" apareciera repetido en cuatro filas de productos, corregir una errata exigiría cuatro UPDATE, y bastaría con olvidar uno para tener dos versiones del mismo dato conviviendo en la tabla. La normalización resuelve eso guardando el nombre una sola vez, en categorias, y dejando en productos únicamente una referencia numérica: categoria_id.

El precio de esa decisión es exactamente el techo del módulo 2:

SELECT id, nombre, categoria_id
FROM productos
WHERE id <= 3;
id nombre categoria_id
1 Aceite de oliva virgen extra 500 ml 1
2 Arroz integral ecológico 1 kg 1
3 Miel de azahar cruda 500 g 1

Un 1 no le sirve a nadie en un informe. El dato existe, pero está repartido entre dos tablas, y hace falta una operación que las vuelva a juntar en el momento de consultar.

La idea clave del módulo: normalizar es separar para almacenar bien; el JOIN es volver a juntar para consultar bien. Son las dos caras de la misma moneda, y por eso una base de datos bien diseñada no es una base de datos incómoda: solo exige aprender a recorrer las relaciones.

  1. Qué es un JOIN: producto cartesiano filtrado

La definición formal de un JOIN cabe en una frase:

Un JOIN es el producto cartesiano de dos tablas, filtrado por una condición.

El producto cartesiano empareja cada fila de la primera tabla con cada fila de la segunda. Si la primera tiene 3 filas y la segunda 2, el resultado tiene 3 × 2 = 6.

Vamos a verlo con dos extractos diminutos de TiendaVerde antes de tocar las tablas completas. Estas son las dos "tablas" de trabajo:

Extracto de categorias (2 filas):

id nombre
4 Bebidas
5 Higiene personal

Extracto de productos (3 filas):

id nombre categoria_id precio
16 Kombucha de jengibre 750 ml 4 4.95
17 Zumo de naranja prensado en frío 1 L 4 5.40
18 Cepillo de dientes de bambú 5 3.50

Paso 1: el producto cartesiano

SELECT p.id   AS producto_id,
       p.nombre AS producto,
       p.categoria_id,
       cat.id AS cat_id,
       cat.nombre AS categoria
FROM productos AS p
CROSS JOIN categorias AS cat
WHERE p.id IN (16, 17, 18)
  AND cat.id IN (4, 5)
ORDER BY p.id, cat.id;
producto_id producto categoria_id cat_id categoria
16 Kombucha de jengibre 750 ml 4 4 Bebidas
16 Kombucha de jengibre 750 ml 4 5 Higiene personal
17 Zumo de naranja prensado en frío 1 L 4 4 Bebidas
17 Zumo de naranja prensado en frío 1 L 4 5 Higiene personal
18 Cepillo de dientes de bambú 5 4 Bebidas
18 Cepillo de dientes de bambú 5 5 Higiene personal

6 filas. Todas las combinaciones posibles. La mayoría son basura: la Kombucha no pertenece a "Higiene personal" y el cepillo de dientes no es una bebida.

Paso 2: quedarse solo con las combinaciones correctas

Las filas buenas son aquellas en las que p.categoria_id coincide con cat.id. Mira la tabla anterior con ese criterio:

producto_id categoria_id cat_id ¿categoria_id = cat_id?
16 4 4 ✅ sí
16 4 5 ❌ no
17 4 4 ✅ sí
17 4 5 ❌ no
18 5 4 ❌ no
18 5 5 ✅ sí

Sobreviven tres filas, una por producto. Eso es exactamente lo que hace un JOIN:

SELECT p.id     AS producto_id,
       p.nombre AS producto,
       cat.nombre AS categoria
FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
WHERE p.id IN (16, 17, 18)
ORDER BY p.id;
producto_id producto categoria
16 Kombucha de jengibre 750 ml Bebidas
17 Zumo de naranja prensado en frío 1 L Bebidas
18 Cepillo de dientes de bambú Higiene personal

Representado como flujo de filas:

flowchart LR
    A["productos<br/>3 filas"] --> C["producto cartesiano<br/>3 × 2 = 6 filas"]
    B["categorias<br/>2 filas"] --> C
    C --> D["filtro ON<br/>p.categoria_id = cat.id"]
    D --> E["resultado<br/>3 filas"]

Un matiz importante sobre el rendimiento. Que el JOIN se defina así no significa que el motor lo ejecute así. PostgreSQL no materializa 300 filas para tirar 280: usa algoritmos como hash join, merge join o nested loop que van directamente a las parejas que casan, normalmente apoyándose en índices. Es la misma distinción entre orden lógico y plan físico que viste en 02-01, y la estudiarás con EXPLAIN en el módulo 8. Para razonar sobre qué devuelve una consulta, el modelo mental "cartesiano + filtro" es siempre correcto.

La consulta completa sobre las 20 filas

Quitado el WHERE de recorte, el JOIN responde por fin a "¿de qué categoría es cada producto?":

SELECT p.id,
       p.nombre   AS producto,
       cat.nombre AS categoria
FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
ORDER BY p.id;
id producto categoria
1 Aceite de oliva virgen extra 500 ml Alimentación
2 Arroz integral ecológico 1 kg Alimentación
3 Miel de azahar cruda 500 g Alimentación
4 Pasta de espelta 500 g Alimentación
5 Tomate triturado ecológico 400 g Alimentación
6 Crema facial de aloe vera 50 ml Cosmética natural
7 Champú sólido de romero 80 g Cosmética natural
8 Aceite corporal de almendras 200 ml Cosmética natural
9 Bálsamo labial de caléndula 15 ml Cosmética natural
10 Detergente ecológico concentrado 1 L Hogar sostenible
11 Estropajo vegetal de luffa (pack 3) Hogar sostenible
12 Bolsas reutilizables de algodón (pack 5) Hogar sostenible
13 Velas de cera de soja (pack 2) Hogar sostenible
14 Infusión de manzanilla ecológica 20 uds Bebidas
15 Té verde matcha ceremonial 30 g Bebidas
16 Kombucha de jengibre 750 ml Bebidas
17 Zumo de naranja prensado en frío 1 L Bebidas
18 Cepillo de dientes de bambú Higiene personal
19 Desodorante natural en barra 50 g Higiene personal
20 Cápsulas de espirulina 120 uds Complementos

20 filas, las mismas que tiene productos. Ese recuento no es casualidad y merece una regla que usarás constantemente:

Cuando unes una tabla con N filas contra otra por una clave foránea obligatoria que apunta a una clave primaria, el resultado tiene exactamente N filas: cada fila encuentra una pareja y solo una. Si el recuento cambia, algo no es como creías.

  1. La cláusula ON y la condición de emparejamiento

ON contiene la condición de emparejamiento: la regla que decide qué fila de la izquierda va con qué fila de la derecha.

FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id

En el 95 % de los casos que escribirás en tu vida, esa condición es exactamente esta forma:

ON <tabla_hija>.<clave_foránea> = <tabla_padre>.<clave_primaria>

Es decir: FK = PK. La razón es evidente si recuerdas 01-05: la clave foránea existe precisamente para señalar a una fila concreta de la otra tabla, así que el camino natural entre dos tablas es el que dibuja la propia FK.

Estas son las condiciones de emparejamiento canónicas de TiendaVerde. Consúltalas cada vez que dudes:

Desde Hacia Condición ON
productos categorias p.categoria_id = cat.id
productos proveedores p.proveedor_id = pr.id
pedidos clientes pe.cliente_id = c.id
pedidos empleados pe.empleado_id = e.id
lineas_pedido pedidos lp.pedido_id = pe.id
lineas_pedido productos lp.producto_id = p.id
resenas productos r.producto_id = p.id
resenas clientes r.cliente_id = c.id
devoluciones pedidos d.pedido_id = pe.id
empleados empleados (jefe) e.jefe_id = jefe.id
clientes clientes (referidor) c.referido_por_id = referidor.id

Aunque la igualdad FK = PK sea lo habitual, ON admite cualquier expresión booleana, igual que WHERE:

-- Condición compuesta: varias igualdades unidas por AND
ON r.producto_id = lp.producto_id AND r.fecha >= pe.fecha_pedido

-- Condición de desigualdad (non-equi join): lo verás en 03-06
ON p1.categoria_id = p2.categoria_id AND p1.id < p2.id

Cuando la condición no es una igualdad se habla de non-equi join. Son minoría, pero existen: rangos de fechas, escalas de precios, comparaciones entre filas de la misma tabla. El caso de p1.id < p2.id lo usarás en la lección 03-06 para generar parejas de productos sin repeticiones.

  1. Sintaxis moderna frente a la antigua de comas

Antes del estándar SQL-92 no existía la palabra JOIN. Las tablas se listaban separadas por comas en el FROM y la condición de emparejamiento se escribía en el WHERE:

-- ⚠️ Sintaxis antigua (SQL-89). Funciona, pero está desaconsejada.
SELECT pe.id,
       pe.fecha_pedido,
       c.nombre,
       c.apellidos
FROM pedidos AS pe, clientes AS c
WHERE pe.cliente_id = c.id
  AND pe.estado = 'pendiente';
-- ✅ Sintaxis moderna (SQL-92 en adelante). La del curso.
SELECT pe.id,
       pe.fecha_pedido,
       c.nombre,
       c.apellidos
FROM pedidos AS pe
JOIN clientes AS c ON pe.cliente_id = c.id
WHERE pe.estado = 'pendiente';
id fecha_pedido nombre apellidos
20 2026-02-21 Camille Dubois

Ambas devuelven lo mismo y PostgreSQL genera el mismo plan para las dos. Pero la antigua tiene cuatro problemas serios:

Problema Explicación
Mezcla dos cosas distintas El WHERE acaba conteniendo condiciones de emparejamiento (pe.cliente_id = c.id) y condiciones de filtrado (pe.estado = 'pendiente') revueltas. Con seis tablas y doce condiciones, distinguir unas de otras es un ejercicio de arqueología
Es fácil olvidar una condición Y el olvido no da error: produce un producto cartesiano silencioso (sección 5)
No permite LEFT/RIGHT/FULL Los JOIN externos, que son la mitad de este módulo, no tienen expresión en la sintaxis de comas. Oracle tenía (+) y SQL Server *= como extensiones propietarias, ambas hoy obsoletas
Rompe la simetría del código Con la sintaxis moderna, cada tabla añadida es una línea JOIN ... ON ... autocontenida. Añadir o quitar una tabla es una edición local

Regla del curso: siempre JOIN ... ON. El FROM describe cómo se relacionan las tablas; el WHERE describe qué filas nos interesan. Nunca se mezclan. La única excepción admitida es el CROSS JOIN deliberado, que veremos en 03-06.

  1. El CROSS JOIN accidental

Este es el motivo práctico por el que la sintaxis de comas se abandonó. Olvida la condición del WHERE:

-- ⚠️ INCORRECTA: falta la condición de emparejamiento
SELECT p.nombre AS producto,
       c.nombre AS cliente
FROM productos AS p, clientes AS c;
(300 filas)

300 filas = 20 productos × 15 clientes. Y lo peor: no hay ningún error. La consulta se ejecuta, devuelve datos con aspecto plausible y, si la metes en un informe, ese informe estará mal sin que nada lo delate.

El mismo olvido con la sintaxis moderna es imposible de cometer sin darse cuenta:

-- ⚠️ INCORRECTA, pero esta vez el motor te para
SELECT p.nombre, c.nombre
FROM productos AS p
JOIN clientes AS c;
ERROR:  syntax error at or near ";"
LINE 3: JOIN clientes AS c;
                          ^

JOIN exige un ON (o un USING). Si de verdad quieres el producto cartesiano tienes que pedirlo explícitamente con CROSS JOIN, que es una declaración de intenciones que nadie escribe por accidente.

El tamaño del accidente crece muy deprisa:

Tabla A Tabla B Filas del cartesiano
categorias (6) proveedores (5) 30
productos (20) clientes (15) 300
lineas_pedido (47) pedidos (20) 940
100 000 100 000 10 000 000 000

Esa última fila es la razón por la que un JOIN mal escrito puede tumbar un servidor. En TiendaVerde solo se traduce en un resultado absurdo; en producción, en una llamada a las tres de la madrugada.

Síntoma inconfundible: si una consulta devuelve muchísimas más filas de las que esperabas y los datos parecen repetirse en bucle, cuenta tus condiciones ON. Con N tablas hacen falta N-1 condiciones de emparejamiento. Tres tablas, dos ON. Cinco tablas, cuatro ON.

  1. USING y NATURAL JOIN

SQL ofrece dos atajos para escribir menos. Uno es útil con reservas; el otro es una trampa.

USING: cuando las columnas se llaman igual

Si la columna de emparejamiento tiene el mismo nombre en ambas tablas, USING (columna) sustituye al ON:

-- Equivalentes cuando ambas tablas tienen una columna llamada producto_id
JOIN resenas AS r ON lp.producto_id = r.producto_id
JOIN resenas AS r USING (producto_id)

USING tiene una propiedad que ON no tiene: fusiona la columna común en una sola, en lugar de devolverla dos veces. Por eso puedes escribirla sin cualificar:

SELECT producto_id,
       lp.id AS linea_id,
       lp.pedido_id,
       r.id AS resena_id,
       r.puntuacion
FROM lineas_pedido AS lp
JOIN resenas AS r USING (producto_id)
WHERE producto_id = 15
ORDER BY lp.id;
producto_id linea_id pedido_id resena_id puntuacion
15 9 4 5 5
15 28 12 5 5
15 42 17 5 5

En TiendaVerde USING es casi inservible, y no por casualidad: el esquema sigue la convención de que la clave primaria se llama id y la foránea <tabla>_id. Como productos.id y lineas_pedido.producto_id no se llaman igual, USING no aplica. Solo funciona entre dos tablas hijas que comparten el nombre de la FK, como el ejemplo de arriba.

ON USING
Nombres de columna Pueden ser distintos Deben ser idénticos
Columna común en el resultado Aparece dos veces Aparece una vez, fusionada
Condiciones compuestas o desigualdades No: solo listas de columnas con igualdad
Uso en TiendaVerde Siempre Excepcional

NATURAL JOIN: el atajo peligroso

NATURAL JOIN va un paso más allá: empareja automáticamente por todas las columnas que se llamen igual en ambas tablas, sin que tú digas cuáles.

-- ⚠️ INCORRECTA en la práctica: no hace lo que parece
SELECT COUNT(*) AS filas
FROM productos NATURAL JOIN categorias;
filas
0

Cero filas. (COUNT(*) simplemente cuenta las filas del resultado; es una función de agregación y se estudia en 04-04. Aquí la usamos como instrumento de medida, y es la única vez que aparecerá en este módulo.)

¿Por qué cero? Porque productos y categorias comparten dos nombres de columna: id y nombre. NATURAL JOIN construye por su cuenta la condición:

ON productos.id = categorias.id AND productos.nombre = categorias.nombre

Es decir, exige que el producto y la categoría tengan el mismo identificador y el mismo nombre. Ninguna pareja lo cumple. La consulta no falla, no avisa: devuelve un resultado vacío perfectamente educado.

Y hay algo peor que un resultado vacío: un resultado que cambia solo. Si mañana alguien añade una columna activo a categorias, ese NATURAL JOIN empezará a emparejar también por activo y devolverá otra cosa, sin que nadie haya tocado la consulta.

Regla del curso: NATURAL JOIN está prohibido. Es un atajo que ahorra veinte caracteres a cambio de que el significado de tu consulta dependa de los nombres de columna que alguien elija en el futuro. En este esquema, además, nombre existe en categorias, proveedores, productos, clientes y empleados: la mina está sembrada.

  1. Alias de tabla y la ambigüedad de nombres

En cuanto hay dos tablas en juego, los nombres de columna pueden repetirse. Y si se repiten, el motor no adivina:

-- ⚠️ INCORRECTA
SELECT id, nombre
FROM productos
JOIN categorias ON productos.categoria_id = categorias.id;
ERROR:  column reference "id" is ambiguous
LINE 1: SELECT id, nombre
               ^

Tanto productos como categorias tienen id y nombre. PostgreSQL no elige por ti: te obliga a cualificar la columna con el nombre (o el alias) de su tabla.

Podrías escribirlo todo con el nombre completo de la tabla:

SELECT productos.id, productos.nombre, categorias.nombre
FROM productos
JOIN categorias ON productos.categoria_id = categorias.id;

Funciona, pero es insoportablemente verboso en cuanto hay cuatro tablas. Los alias de tabla resuelven eso:

-- ✅ CORRECTA
SELECT p.id,
       p.nombre   AS producto,
       cat.nombre AS categoria
FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
ORDER BY p.id
LIMIT 3;
id producto categoria
1 Aceite de oliva virgen extra 500 ml Alimentación
2 Arroz integral ecológico 1 kg Alimentación
3 Miel de azahar cruda 500 g Alimentación

Fíjate en que aquí hacen falta dos tipos de alias distintos, y conviene no confundirlos:

Tipo Dónde va Para qué Ejemplo
Alias de tabla En el FROM/JOIN Cualificar columnas sin escribir el nombre entero productos AS p
Alias de columna En el SELECT Poner nombre a la columna del resultado (02-02) p.nombre AS producto

Sin el alias de columna, el resultado tendría dos columnas llamadas nombre, y ni tú ni tu aplicación sabríais cuál es cuál. Es exactamente el problema que anticipábamos en 02-01 al hablar de SELECT *.

Alias de tabla del curso. Para que todas las consultas del temario se lean igual, fijamos estas abreviaturas y las usaremos siempre:

Tabla Alias Tabla Alias
productos p pedidos pe
categorias cat lineas_pedido lp
proveedores pr resenas r
clientes c devoluciones d
empleados e

Y cuando la misma tabla aparezca dos veces (los self join de 03-06), los alias dejan de ser una comodidad para ser obligatorios, y usaremos nombres significativos: empleados AS e y empleados AS jefe, clientes AS c y clientes AS referidor.

Dos detalles de sintaxis:

  • El AS de los alias de tabla es opcional: FROM productos p es idéntico a FROM productos AS p. En este curso lo escribimos siempre, por coherencia con los alias de columna.
  • Una vez definido el alias, el nombre original deja de ser utilizable. Si escribes FROM productos AS p, no puedes referirte a productos.precio: dará missing FROM-clause entry for table "productos".

  1. Panorámica de los cinco tipos de JOIN

Todos los JOIN comparten el mecanismo de la sección 2. Lo que cambia es qué se hace con las filas que no encuentran pareja.

flowchart TD
    Q{"¿Qué hago con las filas<br/>que no encuentran pareja?"}
    Q -->|"Descartarlas todas"| I["INNER JOIN<br/>03-02"]
    Q -->|"Conservar las de la izquierda"| L["LEFT JOIN<br/>03-03"]
    Q -->|"Conservar las de la derecha"| R["RIGHT JOIN<br/>03-04"]
    Q -->|"Conservar las de ambos lados"| F["FULL OUTER JOIN<br/>03-05"]
    Q -->|"No hay condición:<br/>todas con todas"| C["CROSS JOIN<br/>03-06"]

En forma de tabla, con la pregunta típica de TiendaVerde que resuelve cada uno:

Tipo Qué devuelve Cuándo usarlo Pregunta típica Lección
INNER JOIN Solo las filas que casan a ambos lados Cuando ambos lados son obligatorios para que la fila tenga sentido "¿De qué categoría es cada producto?" 03-02
LEFT JOIN Todas las de la izquierda + las que casen de la derecha (NULL si no casan) Cuando la tabla izquierda es la protagonista y la derecha es opcional "¿Qué ha pedido cada cliente, incluidos los que no han pedido nada?" 03-03
RIGHT JOIN Todas las de la derecha + las que casen de la izquierda Lo mismo, con los papeles invertidos "¿Qué pedidos gestionó cada empleado, incluidos los que no gestionaron ninguno?" 03-04
FULL OUTER JOIN Todas las de ambos lados Conciliar dos fuentes que pueden tener elementos que la otra no tiene "¿Qué hay en el catálogo que no está en ventas, y qué hay en ventas que no está en el catálogo?" 03-05
CROSS JOIN Todas las combinaciones, sin condición Generar combinaciones deliberadamente (calendarios, matrices) "Dame todas las parejas categoría × mes, aunque no hubiera ventas" 03-06

A esos cinco se añade el SELF JOIN, que no es un sexto tipo sino una técnica: usar cualquiera de los anteriores para unir una tabla consigo misma, y así recorrer las relaciones reflexivas de empleados.jefe_id y clientes.referido_por_id. También es 03-06.

Un apunte de vocabulario que verás en la documentación: INNER JOIN es el join interno; LEFT, RIGHT y FULL son los joins externos (outer joins), porque conservan filas que quedan "fuera" del emparejamiento. De ahí que su nombre completo sea LEFT OUTER JOIN, RIGHT OUTER JOIN y FULL OUTER JOIN; la palabra OUTER es opcional en los tres.

  1. Encadenar tres o más tablas

Las preguntas de negocio rara vez se resuelven con dos tablas. Encadenar es simplemente añadir una línea JOIN ... ON ... por cada tabla nueva, y funciona porque el resultado de un JOIN es a su vez una tabla que puede volver a unirse.

flowchart LR
    A["lineas_pedido"] -->|"lp.pedido_id = pe.id"| B["pedidos"]
    B -->|"pe.cliente_id = c.id"| C["clientes"]

Cadena 1: de la línea al cliente

"¿Quién compró cada línea de pedido?" El nombre del cliente no está en lineas_pedido ni es alcanzable en un salto: hay que pasar por pedidos.

SELECT lp.id AS linea_id,
       pe.id AS pedido_id,
       c.nombre || ' ' || c.apellidos AS cliente,
       lp.producto_id,
       lp.cantidad
FROM lineas_pedido AS lp
JOIN pedidos  AS pe ON lp.pedido_id = pe.id
JOIN clientes AS c  ON pe.cliente_id = c.id
ORDER BY lp.id
LIMIT 10;
linea_id pedido_id cliente producto_id cantidad
1 1 Lucía Martínez Soler 1 2
2 1 Lucía Martínez Soler 2 3
3 1 Lucía Martínez Soler 14 2
4 2 Carlos Ferrer Ibáñez 6 1
5 2 Carlos Ferrer Ibáñez 9 2
6 3 Marta Sanchis Gil 5 6
7 3 Marta Sanchis Gil 4 4
8 3 Marta Sanchis Gil 2 2
9 4 Javier Ortega Ruiz 15 1
10 4 Javier Ortega Ruiz 3 1

(10 primeras de 47 filas.)

Tres tablas, dos condiciones ON, y el recuento sigue siendo 47: el mismo que tiene lineas_pedido. Fíjate en que el nombre del cliente se repite en las tres primeras filas, porque el pedido 1 tiene tres líneas. Eso no es un error: es la consecuencia natural de unir por el lado "muchos" de una relación 1:N, y en 03-02 verás por qué es la causa número uno de las sumas infladas del módulo 4.

Cadena 2: de la línea a la categoría

flowchart LR
    A["lineas_pedido"] -->|"lp.producto_id = p.id"| B["productos"]
    B -->|"p.categoria_id = cat.id"| C["categorias"]
SELECT lp.id AS linea_id,
       p.nombre   AS producto,
       cat.nombre AS categoria,
       lp.cantidad,
       ROUND(lp.cantidad * lp.precio_unitario * (1 - lp.descuento), 2) AS importe
FROM lineas_pedido AS lp
JOIN productos  AS p   ON lp.producto_id = p.id
JOIN categorias AS cat ON p.categoria_id = cat.id
ORDER BY lp.id
LIMIT 10;
linea_id producto categoria cantidad importe
1 Aceite de oliva virgen extra 500 ml Alimentación 2 23.90
2 Arroz integral ecológico 1 kg Alimentación 3 11.70
3 Infusión de manzanilla ecológica 20 uds Bebidas 2 6.50
4 Crema facial de aloe vera 50 ml Cosmética natural 1 17.50
5 Bálsamo labial de caléndula 15 ml Cosmética natural 2 9.20
6 Tomate triturado ecológico 400 g Alimentación 6 10.53
7 Pasta de espelta 500 g Alimentación 4 11.20
8 Arroz integral ecológico 1 kg Alimentación 2 7.80
9 Té verde matcha ceremonial 30 g Bebidas 1 22.00
10 Miel de azahar cruda 500 g Alimentación 1 9.75

(10 primeras de 47 filas.)

Ahí está por fin el detalle de ventas legible: qué se vendió, de qué categoría y por cuánto. En el módulo 4 sumaremos esos importes por categoría y responderemos a "¿qué categoría factura más?"; de momento nos quedamos en las filas de detalle, que es lo que este módulo sabe producir.

Observa cómo se calcula el importe siguiendo la convención de 02-02: se opera con precisión completa dentro del ROUND y se redondea solo al presentar. La línea 6 lo ilustra: 6 * 1.95 * 0.90 = 10.53.

Cómo se construye una cadena, en tres pasos. Este método te evitará casi todos los errores:

  1. Localiza el punto de partida: la tabla que contiene el nivel de detalle que quieres (una fila por línea de pedido → lineas_pedido).
  2. Traza el camino en el diagrama ER de 01-06 hasta cada dato que necesites, siguiendo las flechas de las claves foráneas.
  3. Escribe un JOIN ... ON por cada salto, en el orden del camino, y comprueba el recuento de filas al final.

  1. Los JOIN en el orden lógico de ejecución

Aquí llega la ampliación del diagrama que venimos arrastrando desde 02-01, y es la pieza más importante de la lección para lo que viene después.

La pregunta que resuelve es: ¿en qué momento se resuelven los JOIN? Respuesta: dentro del paso FROM, antes de WHERE.

flowchart TD
    subgraph FROM["1 · FROM — se construye el conjunto de partida"]
        direction LR
        A1["1a · tablas base"] --> A2["1b · producto cartesiano<br/>de cada par"]
        A2 --> A3["1c · condición ON<br/>filtra los emparejamientos"]
        A3 --> A4["1d · se añaden las filas<br/>sin pareja (LEFT/RIGHT/FULL)"]
    end
    FROM --> B["2 · WHERE<br/>filtra filas del resultado ya unido"]
    B --> C["3 · SELECT<br/>proyecta y calcula<br/>nacen los alias"]
    C --> D["3b · DISTINCT<br/>elimina duplicados"]
    D --> E["4 · ORDER BY<br/>ordena"]
    E --> F["5 · LIMIT / OFFSET<br/>recorta"]

Léelo despacio, porque de aquí salen tres consecuencias que gobiernan todo el módulo:

1. ON y WHERE se ejecutan en momentos distintos. ON actúa mientras se construye el emparejamiento; WHERE actúa después, sobre la tabla ya combinada.

2. En un INNER JOIN esa diferencia no se nota. Poner pe.estado = 'entregado' en el ON o en el WHERE da exactamente el mismo resultado, porque en un INNER JOIN no hay paso 1d: las filas sin pareja se descartan igual. Lo comprobarás en 03-02.

3. En un LEFT JOIN la diferencia es enorme. El paso 1d reintroduce las filas sin pareja después de aplicar el ON, pero antes de aplicar el WHERE. Resultado:

Dónde pones la condición Qué pasa en un LEFT JOIN
En el ON Restringe qué se empareja. Las filas de la izquierda sin pareja siguen apareciendo, con NULL a la derecha
En el WHERE Filtra el resultado final. Las filas con NULL a la derecha no cumplen la condición y desaparecen: el LEFT JOIN se degrada silenciosamente a INNER JOIN

Es el error más frecuente y más difícil de detectar de todo SQL intermedio, y la lección 03-03 lo demuestra con la misma consulta escrita de las dos formas y sus dos resultados distintos. Ten este diagrama a mano cuando llegues allí.

Una nota final sobre el orden dentro del propio FROM: cuando encadenas varios JOIN, se resuelven de izquierda a derecha. A JOIN B JOIN C significa (A JOIN B) JOIN C: primero se une A con B, y el resultado se une con C. Con INNER JOIN puros el orden es indiferente (03-02); con LEFT JOIN encadenados, no lo es en absoluto (03-03).

Errores Comunes y Consejos

  • Olvidar la condición ON usando la sintaxis de comas. Produce un producto cartesiano silencioso: muchas filas, ningún error. Con N tablas necesitas N-1 condiciones de emparejamiento.
  • column reference "id" is ambiguous. Dos tablas del FROM tienen una columna con ese nombre. Cualifícala: p.id, cat.id. Ocurre siempre con id y con nombre en este esquema.
  • missing FROM-clause entry for table "productos". Has definido el alias p y luego has escrito productos.precio. Una vez hay alias, el nombre original ya no existe para esa consulta.
  • Usar NATURAL JOIN "porque es más corto". Empareja por todas las columnas homónimas, incluidas las que alguien añada mañana. En productos NATURAL JOIN categorias devuelve 0 filas por culpa de nombre.
  • Emparejar por la columna equivocada. ON lp.producto_id = pe.id compila perfectamente y devuelve basura: estás comparando un identificador de producto con uno de pedido. Comprueba siempre que ambos lados de la igualdad hablan de la misma entidad.
  • Suponer que un JOIN nunca cambia el número de filas. Solo se conserva cuando unes por FK obligatoria contra PK. Si la FK admite NULL, pierdes filas (03-03); si el lado derecho tiene varias filas por cada izquierda, las multiplicas (03-02).
  • Mezclar sintaxis antigua y moderna en la misma consulta. FROM a, b JOIN c ON ... es legal y es un campo de minas de precedencia. No lo hagas.
  • Consejo: escribe el JOIN antes que el SELECT. Construye primero el FROM con sus cadenas, ejecuta con SELECT * y LIMIT 5 para ver la forma del resultado, y solo entonces elige las columnas.
  • Consejo: cuenta las filas después de cada JOIN que añadas. Si añades una tabla y el recuento se dispara, esa tabla tiene varias filas por cada fila anterior. Si cae a cero, tu condición ON no casa nada.
  • Consejo: ten abierto el diagrama ER de 01-06. Los JOIN son caminos por ese diagrama. Nadie los memoriza; se leen.

Ejercicios

Ejercicio 1

Compras necesita saber qué parte del catálogo depende de proveedores extranjeros. Escribe una consulta que devuelva, solo para los productos cuyo proveedor no es de España: el id y el nombre del producto, su precio, el nombre del proveedor y su país. Ordena por país y, dentro de cada país, por id de producto.

Después responde: ¿por qué la condición sobre el país va en el WHERE y no en el ON?

Ejercicio 2

Te llega esta consulta escrita por un compañero:

SELECT pe.id, pe.fecha_pedido, c.nombre, c.apellidos, c.ciudad
FROM pedidos pe, clientes c
WHERE pe.cliente_id = c.id
  AND c.pais = 'Francia'
  AND pe.estado = 'entregado';
  1. Reescríbela con sintaxis moderna JOIN ... ON, separando claramente emparejamiento y filtrado.
  2. ¿Qué ocurriría exactamente si alguien borrara por accidente la línea pe.cliente_id = c.id en la versión original? Calcula cuántas filas devolvería.

Ejercicio 3

Sin ejecutar nada, predice el número de filas que devuelve cada una de estas consultas y justifica cada predicción en una frase. Después ejecútalas y comprueba.

-- a)
SELECT * FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id;

-- b)
SELECT * FROM productos AS p, categorias AS cat;

-- c)
SELECT * FROM lineas_pedido AS lp
JOIN pedidos AS pe ON lp.pedido_id = pe.id;

-- d)
SELECT * FROM pedidos AS pe
JOIN empleados AS e ON pe.empleado_id = e.id;

-- e)
SELECT * FROM resenas AS r
JOIN clientes AS c ON r.cliente_id = c.id;

Soluciones

Solución 1

SELECT p.id,
       p.nombre  AS producto,
       p.precio,
       pr.nombre AS proveedor,
       pr.pais
FROM productos AS p
JOIN proveedores AS pr ON p.proveedor_id = pr.id
WHERE pr.pais <> 'España'
ORDER BY pr.pais, p.id;
id producto precio proveedor pais
10 Detergente ecológico concentrado 1 L 11.20 EcoNordic Supplies Alemania
13 Velas de cera de soja (pack 2) 13.75 EcoNordic Supplies Alemania
18 Cepillo de dientes de bambú 3.50 EcoNordic Supplies Alemania
20 Cápsulas de espirulina 120 uds 16.40 EcoNordic Supplies Alemania
6 Crema facial de aloe vera 50 ml 18.90 Maison Nature Francia
7 Champú sólido de romero 80 g 8.40 Maison Nature Francia
9 Bálsamo labial de caléndula 15 ml 4.60 Maison Nature Francia
19 Desodorante natural en barra 50 g 7.80 Maison Nature Francia
8 Aceite corporal de almendras 200 ml 14.25 Verde Atlántico Portugal
11 Estropajo vegetal de luffa (pack 3) 5.50 Verde Atlántico Portugal
12 Bolsas reutilizables de algodón (pack 5) 9.90 Verde Atlántico Portugal
15 Té verde matcha ceremonial 30 g 22.00 Verde Atlántico Portugal

12 filas de las 20 del catálogo: 8 productos vienen de los dos proveedores españoles (Huerta del Turia y BioSierra Ibérica) y los otros 12 de Alemania, Francia y Portugal.

Por qué la condición va en el WHERE: pr.pais <> 'España' no dice cómo se emparejan las tablas —eso ya lo dice p.proveedor_id = pr.id—, dice qué filas del resultado nos interesan. Es la separación de responsabilidades de la sección 4: ON para emparejar, WHERE para filtrar.

Dicho esto, en este caso concreto ponerla en el ON daría el mismo resultado, porque es un INNER JOIN y los pasos 1c y 2 del orden lógico se comportan igual cuando no hay filas huérfanas que reintroducir. La distinción se vuelve crítica en el LEFT JOIN de 03-03, y por eso conviene coger la costumbre correcta desde ahora.

Solución 2

1. Reescritura moderna:

SELECT pe.id,
       pe.fecha_pedido,
       c.nombre,
       c.apellidos,
       c.ciudad
FROM pedidos AS pe
JOIN clientes AS c ON pe.cliente_id = c.id
WHERE c.pais = 'Francia'
  AND pe.estado = 'entregado';
id fecha_pedido nombre apellidos ciudad
10 2025-08-03 Camille Dubois Lyon
12 2025-10-01 Julien Moreau París

Los dos pedidos entregados de clientes franceses. La reescritura deja a la vista lo que la versión antigua escondía: una sola condición de emparejamiento y dos de filtrado.

2. Si se borra pe.cliente_id = c.id:

La consulta pasa a ser un producto cartesiano filtrado solo por país y estado. El cálculo:

  • Clientes de Francia: 2 (Camille Dubois y Julien Moreau).
  • Pedidos con estado entregado: 14.
  • Filas resultantes: 2 × 14 = 28.

Y serían 28 filas falsas: cada pedido entregado aparecería asociado a los dos clientes franceses, incluidos los pedidos que en realidad son de Lucía, de Sofia o de Tiago. Ningún error, ningún aviso, un informe completamente inventado. Es exactamente el peligro de la sección 5.

Solución 3

# Filas Justificación
a) 20 Cada producto tiene una categoria_id que apunta a una categoría existente: una pareja por producto. El recuento de la tabla de partida se conserva
b) 120 Producto cartesiano: 20 productos × 6 categorías. No hay condición que filtre
c) 47 Cada línea pertenece a un pedido y pedido_id es NOT NULL: una pareja por línea. El recuento de lineas_pedido se conserva
d) 10 Aquí se pierden filas. De los 20 pedidos, 10 tienen empleado_id IS NULL (los pedidos web). Un NULL no es igual a nada —ni siquiera a otro NULL, como viste en 02-03—, así que esas 10 filas no encuentran pareja y el INNER JOIN las descarta
e) 12 Cada reseña tiene un cliente_id obligatorio y válido: una pareja por reseña

El caso d) es el más instructivo de los cinco y el motivo de que existan las lecciones 03-03 y 03-04: si la dirección te pide "el listado de pedidos con su comercial" y entregas 10 filas de 20, has perdido la mitad del negocio sin enterarte.

Conclusión

Ya tienes el marco conceptual completo del módulo:

  • La normalización separa para almacenar sin redundancia; el JOIN recompone para consultar. Son complementarios, no contradictorios.
  • Un JOIN es un producto cartesiano filtrado por una condición. Ese modelo mental —combinar todo con todo y quedarse con lo que casa— explica el comportamiento de todos los tipos de JOIN, aunque el motor lo ejecute de forma mucho más eficiente.
  • La condición va en ON y casi siempre tiene la forma FK = PK. Tienes la tabla de las once condiciones canónicas de TiendaVerde para consultarla siempre que dudes.
  • La sintaxis moderna JOIN ... ON sustituye a la antigua de comas: separa emparejamiento de filtrado, impide el CROSS JOIN accidental y es la única que admite JOIN externos.
  • USING solo sirve si las columnas se llaman igual (raro en este esquema) y NATURAL JOIN está prohibido: en productos NATURAL JOIN categorias devuelve 0 filas por culpa de la columna nombre.
  • Los alias de tabla son obligatorios en la práctica: sin ellos llegan los column reference "id" is ambiguous. Tienes fijados los del curso: p, cat, pr, c, e, pe, lp, r, d.
  • Conoces la panorámica de los cinco tipos y sabes que la única diferencia entre ellos es qué se hace con las filas que no encuentran pareja.
  • Sabes encadenar tres o más tablas siguiendo el diagrama ER, con N-1 condiciones ON para N tablas, y tienes los dos caminos que usarás todo el curso: lineas_pedido → pedidos → clientes y lineas_pedido → productos → categorias.
  • Y sabes que, en el orden lógico, los JOIN se resuelven dentro del paso FROM, antes del WHERE: de ahí saldrá la diferencia crítica entre poner una condición en ON o en WHERE.

En la lección siguiente, INNER JOIN, bajaremos al detalle del tipo que acabas de usar sin nombrarlo: qué filas conserva, cuáles pierde y por qué. Verás desaparecer a los clientes 13, 14 y 15 al unir clientes con pedidos, y a los 10 pedidos web al unir pedidos con empleados. Esas desapariciones, lejos de ser un fallo, son la definición misma del INNER JOIN, y entenderlas es lo que hace que el LEFT JOIN de 03-03 tenga sentido.

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