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.
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.
-- 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)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.
-- 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);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.
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.
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;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.
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.
SELECT c.nombre, p.fecha, p.total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.nombre;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.
Solo las filas que tienen pareja en las dos tablas.
Para «las ventas con su cliente»: una venta sin cliente no debería existir.
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.
«¿Quiero ver también los que no tienen nada al otro lado?»
Si la respuesta es sí, LEFT JOIN. Es toda la regla.
-- 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';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.
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.
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.
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.
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?
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