Relaciones y JOIN
💡 🎯 Al terminar esta lección podrás…
- Crear claves foráneas con
REFERENCESy entender la integridad referencial. - Distinguir relaciones uno a muchos y muchos a muchos.
- Unir tablas con
INNER JOINyLEFT JOIN. - Encontrar filas sin relación con
LEFT JOIN … IS NULL.
1.Por qué separar los datos en varias tablas
En la lección 4 viste la regla «un dato, un lugar». Si cada pedido repitiera el nombre y el correo del cliente, un cambio de correo obligaría a tocar muchas filas. La solución es guardar al cliente una sola vez y, en el pedido, solo su id. Ese id escrito en otra tabla se llama clave foránea.
Usaremos cuatro tablas. productos ya la conoces; las otras tres son nuevas (clientes es una versión simplificada de la de la lección 4):
📌 Nota · Los datos de ejemplo
En esta lección y en las siguientes hay 4 clientes (Ana, Luis, Marta y Diego), 4 pedidos y 7 líneas de pedido. Diego todavía no ha comprado nada: lo usaremos para ver qué hace cada tipo de JOIN.
2.Claves foráneas: REFERENCES
Una clave foránea es una columna cuyo valor debe existir como clave primaria en otra tabla. Se declara con REFERENCES:
CREATE TABLE clientes (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE pedidos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id),
fecha TEXT NOT NULL
);
CREATE TABLE lineas_pedido (
pedido_id INTEGER NOT NULL REFERENCES pedidos(id),
producto_id INTEGER NOT NULL REFERENCES productos(id),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
PRIMARY KEY (pedido_id, producto_id)
);
Lee cliente_id INTEGER NOT NULL REFERENCES clientes(id) como: «cada pedido pertenece a un cliente que debe existir en clientes». Con esto la base de datos protege la integridad: no deja crear un pedido para un cliente inexistente.
INSERT INTO pedidos (cliente_id, fecha)
VALUES (99, '2026-10-05');
Resultado esperado (error real de SQLite):
Error: FOREIGN KEY constraint failed
Lo mismo ocurre al revés: no puedes borrar un cliente que aún tiene pedidos.
DELETE FROM clientes WHERE id = 1;
Resultado esperado (error real de SQLite):
Error: FOREIGN KEY constraint failed
⚠️ Importante · SQLite y PostgreSQL
PostgreSQL siempre hace cumplir las claves foráneas. SQLite las ignora por defecto: debes activarlas en cada conexión con PRAGMA foreign_keys = ON;. Los errores de esta lección salen con esa opción activada.
3.Tipos de relaciones
Según cuántas filas de una tabla se asocian con filas de otra, hay tres tipos:
| Relación | Ejemplo | Cómo se guarda |
|---|---|---|
| Uno a muchos | Un cliente tiene muchos pedidos | Clave foránea en el lado «muchos»: pedidos.cliente_id |
| Muchos a muchos | Un pedido incluye varios productos y un producto aparece en varios pedidos | Una tabla intermedia con dos claves foráneas: lineas_pedido |
| Uno a uno | Un usuario tiene un perfil | Clave foránea con UNIQUE (poco frecuente) |
En lineas_pedido, la clave primaria es la pareja (pedido_id, producto_id): un producto solo puede aparecer una vez en un mismo pedido, con su cantidad. Estos son los datos:
Contenido de lineas_pedido
| pedido_id | producto_id | cantidad |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 2 | 1 |
| 2 | 5 | 1 |
| 3 | 7 | 1 |
| 3 | 8 | 2 |
| 4 | 6 | 2 |
| 4 | 2 | 3 |
Hasta aquí solo vemos números. Para ver nombres hay que unir las tablas.
4.INNER JOIN: unir filas que coinciden
JOIN combina filas de dos tablas según una condición, escrita tras ON. Con INNER JOIN (o solo JOIN) quedan únicamente las filas que tienen pareja en ambas tablas.
SELECT c.nombre, p.id AS pedido, p.fecha
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
ORDER BY p.id;
c y p son alias de tabla: abreviaturas para no repetir nombres largos y evitar ambigüedades (c.id frente a p.id). AS pedido renombra la columna en el resultado.
Resultado esperado
| nombre | pedido | fecha |
|---|---|---|
| Ana Torres | 1 | 2026-09-01 |
| Ana Torres | 2 | 2026-09-15 |
| Luis Pérez | 3 | 2026-09-20 |
| Marta Ruiz | 4 | 2026-10-02 |
Diego no aparece: no tiene pedidos, así que no hay pareja para él.
5.LEFT JOIN: conservar todo el lado izquierdo
LEFT JOIN conserva todas las filas de la tabla izquierda (la de FROM) aunque no tengan pareja; en ese caso las columnas de la derecha valen NULL.
SELECT c.nombre, p.id AS pedido, p.fecha
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
Resultado esperado
| nombre | pedido | fecha |
|---|---|---|
| Ana Torres | 1 | 2026-09-01 |
| Ana Torres | 2 | 2026-09-15 |
| Luis Pérez | 3 | 2026-09-20 |
| Marta Ruiz | 4 | 2026-10-02 |
| Diego Soto | NULL | NULL |
Esto permite una consulta muy útil: encontrar lo que no tiene relación, filtrando las filas con NULL en la tabla de la derecha (recuerda usar IS NULL, no = NULL).
SELECT c.nombre
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;
Resultado esperado
| nombre |
|---|
| Diego Soto |
📌 Nota · ¿Y RIGHT y FULL JOIN?
RIGHT JOIN es el espejo de LEFT JOIN y FULL JOIN conserva ambos lados. PostgreSQL los admite siempre; SQLite solo desde la versión 3.39. En la práctica casi siempre basta con reordenar las tablas y usar LEFT JOIN.
6.Unir tres o más tablas
Puedes encadenar JOIN. Para saber qué contiene el pedido 4 hay que pasar por la tabla intermedia:
SELECT p.id AS pedido, pr.nombre AS producto, l.cantidad, pr.precio
FROM pedidos p
JOIN lineas_pedido l ON l.pedido_id = p.id
JOIN productos pr ON pr.id = l.producto_id
WHERE p.id = 4;
Resultado esperado
| pedido | producto | cantidad | precio |
|---|---|---|---|
| 4 | Ratón | 3 | 45.50 |
| 4 | Altavoz | 2 | 99.00 |
Y con cuatro tablas obtienes el historial completo de compras:
SELECT c.nombre, p.fecha, pr.nombre AS producto, l.cantidad
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
JOIN lineas_pedido l ON l.pedido_id = p.id
JOIN productos pr ON pr.id = l.producto_id
ORDER BY p.fecha, pr.nombre;
Resultado esperado
| nombre | fecha | producto | cantidad |
|---|---|---|---|
| Ana Torres | 2026-09-01 | Ratón | 1 |
| Ana Torres | 2026-09-01 | Teclado | 1 |
| Ana Torres | 2026-09-15 | Webcam | 1 |
| Luis Pérez | 2026-09-20 | Cable HDMI | 2 |
| Luis Pérez | 2026-09-20 | Monitor 4K | 1 |
| Marta Ruiz | 2026-10-02 | Altavoz | 2 |
| Marta Ruiz | 2026-10-02 | Ratón | 3 |
Si ves filas de más o repetidas, casi siempre falta o sobra una condición ON.
7.Pruébalo: laboratorio de JOIN
Cambia el tipo de unión y observa qué filas aparecen. Las filas con NULL (clientes sin pedido) se resaltan. Los resultados son los de SQLite real sobre estos datos.
🧪 Ejemplo · Experimentos
1) Con INNER JOIN cuenta las filas y localiza a Diego: no está. 2) Cambia a LEFT JOIN: aparece con NULL. 3) Marca «Solo sin pareja» con INNER JOIN: no queda ninguna fila, porque el inner solo conserva parejas. 4) Haz lo mismo con LEFT JOIN: queda solo Diego.
8.Errores frecuentes
- Olvidar la condición
ON: sin ella cada fila se combina con todas las de la otra tabla (producto cartesiano) y el resultado explota. - Columnas ambiguas: escribir
idcuando ambas tablas lo tienen da error. Usac.idop.id. - Esperar a Diego con
INNER JOIN: si quieres conservar filas sin pareja, usaLEFT JOIN. - Filtrar el lado derecho de un
LEFT JOINenWHERE: una condición comoWHERE p.fecha > '2026-09-10'descarta las filasNULLy lo convierte en unINNER JOIN. Si debe afectar a la unión, ponla en elON. - Comparar con
= NULLen lugar deIS NULL: nunca encuentra nada. - Olvidar
PRAGMA foreign_keys = ONen SQLite: las claves foráneas no se comprueban y puedes crear datos huérfanos. - Unir por la columna equivocada (
p.id = c.iden lugar dep.cliente_id = c.id): da resultados que parecen válidos pero son incorrectos.
📒 Apuntes para llevar
- Una clave foránea es una columna que apunta a la clave primaria de otra tabla:
cliente_id INTEGER REFERENCES clientes(id). - Impide datos huérfanos: no se crea un pedido de un cliente inexistente ni se borra un cliente con pedidos. En SQLite activa
PRAGMA foreign_keys = ON. - Uno a muchos: la clave foránea va en el lado «muchos». Muchos a muchos: tabla intermedia con dos claves foráneas.
INNER JOIN … ON …devuelve solo las filas con pareja en ambas tablas.LEFT JOINconserva todas las filas de la tabla izquierda; sin pareja, las columnas de la derecha sonNULL.LEFT JOIN … WHERE derecha.id IS NULLencuentra filas sin relación (clientes sin pedidos).- Usa alias cortos (
c,p) y califica las columnas (c.nombre) cuando unes varias tablas.
9.Glosario
| Término | Significado |
|---|---|
| Clave foránea | Columna que guarda la clave primaria de una fila de otra tabla para relacionarlas. |
| Integridad referencial | Garantía de que toda clave foránea apunta a una fila que existe. |
| JOIN | Operación que combina filas de dos tablas según una condición. |
| INNER JOIN | Unión que conserva solo las filas con pareja en ambas tablas. |
| LEFT JOIN | Unión que conserva todas las filas de la tabla izquierda; completa con NULL si no hay pareja. |
| Alias | Nombre corto temporal para una tabla o columna (clientes c, AS pedido). |
| Tabla intermedia | Tabla que implementa una relación muchos a muchos con dos claves foráneas. |
| Producto cartesiano | Todas las combinaciones posibles entre filas de dos tablas; aparece al omitir la condición del JOIN. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Qué hace cliente_id INTEGER REFERENCES clientes(id)?
Es una clave foránea: la base de datos rechaza valores que no correspondan a un cliente existente.
¿Cómo se representa una relación muchos a muchos entre pedidos y productos?
La tabla intermedia (lineas_pedido) convierte la relación en dos relaciones uno a muchos.
Con 4 clientes, de los cuales uno no tiene pedidos, ¿qué diferencia hay entre INNER JOIN y LEFT JOIN de clientes a pedidos?
El LEFT JOIN conserva todas las filas de la izquierda; el INNER JOIN solo las que tienen pareja.
¿Cómo encuentras los clientes que nunca han hecho un pedido?
Se unen con LEFT JOIN y se queda con las filas sin pareja usando IS NULL.
¿Qué ocurre si olvidas la condición ON en un JOIN?
Sin condición se obtiene el producto cartesiano: filas de más y sin sentido.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.