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
- Por qué hay que recomponer lo que la normalización separó
- Qué es un
JOIN: producto cartesiano filtrado - La cláusula
ONy la condición de emparejamiento - Sintaxis moderna frente a la antigua de comas
- El
CROSS JOINaccidental USINGyNATURAL JOIN- Alias de tabla y la ambigüedad de nombres
- Panorámica de los cinco tipos de
JOIN - Encadenar tres o más tablas
- Los
JOINen el orden lógico de ejecución - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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:
| 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
JOINes 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.
- Qué es un
JOIN: producto cartesiano filtrado
JOIN: producto cartesiano filtradoLa definición formal de un JOIN cabe en una frase:
Un
JOINes 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.
- La cláusula
ON y la condición de emparejamiento
ON y la condición de emparejamientoON contiene la condición de emparejamiento: la regla que decide qué fila de la izquierda va con qué fila de la derecha.
En el 95 % de los casos que escribirás en tu vida, esa condición es exactamente esta forma:
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.idCuando 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.
- 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. ElFROMdescribe cómo se relacionan las tablas; elWHEREdescribe qué filas nos interesan. Nunca se mezclan. La única excepción admitida es elCROSS JOINdeliberado, que veremos en 03-06.
- El
CROSS JOIN accidental
CROSS JOIN accidentalEste 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 = 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;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, dosON. Cinco tablas, cuatroON.
USING y NATURAL JOIN
USING y NATURAL JOINSQL 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 | Sí | 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:
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 JOINestá 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,nombreexiste encategorias,proveedores,productos,clientesyempleados: la mina está sembrada.
- 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;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
ASde los alias de tabla es opcional:FROM productos pes idéntico aFROM 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 aproductos.precio: darámissing FROM-clause entry for table "productos".
- Panorámica de los cinco tipos de
JOIN
JOINTodos 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.
- 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:
- Localiza el punto de partida: la tabla que contiene el nivel de detalle que quieres (una fila por línea de pedido →
lineas_pedido). - Traza el camino en el diagrama ER de 01-06 hasta cada dato que necesites, siguiendo las flechas de las claves foráneas.
- Escribe un
JOIN ... ONpor cada salto, en el orden del camino, y comprueba el recuento de filas al final.
- Los
JOIN en el orden lógico de ejecución
JOIN en el orden lógico de ejecuciónAquí 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
ONusando 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 delFROMtienen una columna con ese nombre. Cualifícala:p.id,cat.id. Ocurre siempre conidy connombreen este esquema.missing FROM-clause entry for table "productos". Has definido el aliaspy luego has escritoproductos.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. Enproductos NATURAL JOIN categoriasdevuelve 0 filas por culpa denombre. - Emparejar por la columna equivocada.
ON lp.producto_id = pe.idcompila 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
JOINnunca cambia el número de filas. Solo se conserva cuando unes por FK obligatoria contra PK. Si la FK admiteNULL, 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
JOINantes que elSELECT. Construye primero elFROMcon sus cadenas, ejecuta conSELECT *yLIMIT 5para ver la forma del resultado, y solo entonces elige las columnas. - Consejo: cuenta las filas después de cada
JOINque 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ónONno casa nada. - Consejo: ten abierto el diagrama ER de 01-06. Los
JOINson 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';- Reescríbela con sintaxis moderna
JOIN ... ON, separando claramente emparejamiento y filtrado. - ¿Qué ocurriría exactamente si alguien borrara por accidente la línea
pe.cliente_id = c.iden 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
JOINrecompone para consultar. Son complementarios, no contradictorios. - Un
JOINes 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 deJOIN, aunque el motor lo ejecute de forma mucho más eficiente. - La condición va en
ONy 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 ... ONsustituye a la antigua de comas: separa emparejamiento de filtrado, impide elCROSS JOINaccidental y es la única que admiteJOINexternos. USINGsolo sirve si las columnas se llaman igual (raro en este esquema) yNATURAL JOINestá prohibido: enproductos NATURAL JOIN categoriasdevuelve 0 filas por culpa de la columnanombre.- 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
ONpara N tablas, y tienes los dos caminos que usarás todo el curso:lineas_pedido → pedidos → clientesylineas_pedido → productos → categorias. - Y sabes que, en el orden lógico, los
JOINse resuelven dentro del pasoFROM, antes delWHERE: de ahí saldrá la diferencia crítica entre poner una condición enONo enWHERE.
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
- ¿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
