Bases de datos SQL

Relaciones y JOIN

Lección 6 de 8 · 17 min

Por qué no se guarda todo en una tabla

Imagina una única tabla de pedidos donde cada fila lleva el nombre del cliente, su ciudad y su teléfono. Funciona… hasta que el cliente cambia de teléfono. Entonces hay que cambiarlo en las cuarenta filas de sus pedidos, y basta con que se escape una para tener dos teléfonos distintos del mismo cliente.

Ese es el problema: el dato repetido acaba siendo dato contradictorio. Y ya no hay forma de saber cuál es el bueno.

La solución es guardar cada cosa una sola vez: los clientes en su tabla, los pedidos en la suya, y en cada pedido una referencia al cliente al que pertenece. Cambiar el teléfono pasa a ser una fila, no cuarenta.

La tabla de pedidos, apuntando a clientes
-- PostgreSQL
CREATE TABLE pedidos (
    id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    cliente_id  INTEGER NOT NULL REFERENCES clientes(id),
    fecha       DATE NOT NULL DEFAULT CURRENT_DATE,
    total       DECIMAL(12,2) NOT NULL,
    estado      VARCHAR(20) NOT NULL DEFAULT 'pendiente'
);

-- La misma idea, escrita en la forma larga (vale en los tres motores)
-- CONSTRAINT fk_pedidos_cliente FOREIGN KEY (cliente_id) REFERENCES clientes(id)

Qué hace realmente una clave foránea

cliente_id guarda el id de un cliente. Pero la palabra REFERENCES hace algo más que documentarlo: convierte esa relación en una regla que la base de datos impone.

La regla en acción
-- El cliente 3 existe: entra sin problema
INSERT INTO pedidos (cliente_id, total) VALUES (3, 450000);

-- El cliente 999 NO existe: la base de datos lo rechaza
INSERT INTO pedidos (cliente_id, total) VALUES (999, 120000);
Resultado
INSERT 0 1

ERROR:  insert or update on table "pedidos" violates foreign key constraint
DETAIL:  Key (cliente_id)=(999) is not present in table "clientes".

Eso es lo que impide los datos huérfanos: pedidos que apuntan a un cliente que no existe. Sin la clave foránea, ese INSERT habría entrado tan tranquilo, y el error aparecería meses después al hacer un informe que no cuadra.

La clave foránea también protege por el otro lado: no deja borrar un cliente que tiene pedidos. Es lo correcto, aunque en el momento moleste.

Existe la opción ON DELETE CASCADE, que al borrar el cliente borra automáticamente todos sus pedidos. Suena cómodo y es una decisión seria: se activa una vez y un día alguien borra un cliente y desaparece su historial entero sin ningún aviso. Para datos con valor contable, casi siempre es preferible marcar el cliente como inactivo y no borrar nada.

JOIN: volver a juntarlo

Los datos están repartidos, y para leerlos hay que unirlos otra vez. Eso es un JOIN: decirle a la base de datos por qué columna se corresponden las dos tablas.

Y aquí hay una buena noticia: JOIN se escribe igual en los tres motores.

INNER JOIN: lo que coincide en las dos
SELECT c.nombre, c.ciudad, p.fecha, p.total
FROM pedidos p
INNER JOIN clientes c ON p.cliente_id = c.id
ORDER BY p.fecha DESC;
Resultado
nombre        ciudad     fecha        total
------------  ---------  ----------  --------
Julia Gomez   Bogota     2026-09-05   450000
Ana Martinez  Bogota     2026-09-03   120000
Julia Gomez   Bogota     2026-08-28    89000

Esas letras p y c son alias: un apodo corto para cada tabla. No son adorno: cuando las dos tablas tienen una columna llamada id, hay que decir de cuál hablas, y p.cliente_id = c.id lo deja claro.

La diferencia que produce informes equivocados

Aquí está el fallo silencioso de esta lección. INNER JOIN devuelve solo lo que coincide en ambas tablas. Los clientes que todavía no han hecho ningún pedido desaparecen del resultado.

Y no hay error ni aviso: simplemente el informe sale con menos clientes de los que tiene la empresa.

LEFT JOIN: todos los de la izquierda, coincidan o no
SELECT c.nombre, p.fecha, p.total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.nombre;
Resultado
nombre         fecha        total
-------------  ----------  --------
Ana Martinez   2026-09-03   120000
Carlos Ruiz    NULL          NULL
Julia Gomez    2026-09-05   450000
Luis Herrera   NULL          NULL

Ahí están Carlos y Luis, con NULL. Son los clientes sin pedidos, y con INNER JOIN no habrían aparecido.

Los NULL no son un error: significan «este cliente no tiene ningún pedido», que es justo lo que querías saber. De hecho, la forma de encontrar a los clientes inactivos es un LEFT JOIN con WHERE p.id IS NULL.

Cuál usar, en una frase

INNER JOIN

Solo las filas que tienen pareja en las dos tablas.

Para «las ventas con su cliente»: una venta sin cliente no debería existir.

LEFT JOIN

Todas las de la primera tabla, tengan pareja o no.

Para «todos los clientes y sus pedidos, si los tienen». Es el que se usa en informes donde no puede faltar nadie.

La pregunta que decide

«¿Quiero ver también los que no tienen nada al otro lado?»

Si la respuesta es sí, LEFT JOIN. Es toda la regla.

Los dos usos que resuelven el 90% de los casos
-- Clientes que NUNCA han pedido nada
SELECT c.nombre, c.ciudad
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;

-- Unir tres tablas: se encadenan los JOIN
SELECT c.nombre, p.fecha, d.cantidad, a.descripcion
FROM pedidos p
INNER JOIN clientes c        ON p.cliente_id = c.id
INNER JOIN pedido_detalle d  ON d.pedido_id  = p.id
INNER JOIN articulos a       ON a.id         = d.articulo_id
WHERE p.fecha >= '2026-09-01';

Problemas típicos con JOIN

El resultado tiene muchísimas más filas de las esperadas

Casi siempre es un JOIN al que se le olvidó la condición ON, o una condición incompleta.

Sin ON, la base de datos combina cada fila con todas las de la otra tabla: 100 clientes por 500 pedidos son 50.000 filas. Es el «producto cartesiano», y se nota enseguida por el volumen.

Column reference is ambiguous

Las dos tablas tienen una columna con el mismo nombre -típicamente id o nombre- y no dijiste de cuál hablabas.

Se arregla con los alias: c.nombre en vez de nombre.

Filtrar en el WHERE convierte mi LEFT JOIN en INNER

Es sutil y muerde a todo el mundo: si pones una condición sobre la tabla de la derecha en el WHERE, las filas con NULL no la cumplen y se caen del resultado.

La condición sobre la tabla derecha va dentro del ON, no en el WHERE.

¿Y RIGHT JOIN y FULL JOIN?

RIGHT JOIN es el espejo del LEFT y casi no se usa: se prefiere darle la vuelta al orden de las tablas, que se lee mejor. FULL JOIN trae todo de ambos lados.

Con INNER y LEFT cubres casi todo lo que vas a necesitar.

Compruébalo tú mismo

Haces un informe de todos los clientes con su total de pedidos usando INNER JOIN. ¿Qué problema tiene?

Intentas insertar un pedido con cliente_id = 999, que no existe, y la base de datos lo rechaza. ¿Por qué?

Un JOIN devuelve 50.000 filas cuando esperabas 500. ¿Qué revisas?

Lo que sigue

Ya sabes unir tablas. La siguiente lección resume: contar, sumar y promediar por grupos con GROUP BY, la diferencia entre WHERE y HAVING, y las funciones de fecha y texto, que es donde los tres motores vuelven a separarse.

Siguiente: Agrupar, fechas y texto

Continuar ← Ordenar, limitar, actualizar y borrar