Volver a SQL Básico
JOINs y Relaciones
Los JOINs combinan filas de dos o más tablas basándose en una columna relacionada.
Tipos de JOINs
Tabla A: Tabla B:
┌───┐ ┌───┐
│ 1 │ │ 1 │
│ 2 │ │ 2 │
│ 3 │ │ 4 │
└───┘ └───┘
INNER JOIN → filas en ambas: 1, 2
LEFT JOIN → todo de A: 1, 2, 3 (3 tiene NULL en cols de B)
RIGHT JOIN → todo de B: 1, 2, 4 (4 tiene NULL en cols de A)
FULL JOIN → todas las filas: 1, 2, 3, 4
INNER JOIN
Devuelve solo filas donde la condición coincide en ambas tablas:
-- Pedidos con nombres de cliente
SELECT
p.id AS id_pedido,
c.nombre,
c.apellido,
p.total,
p.creado_en
FROM pedidos p
INNER JOIN clientes c ON p.cliente_id = c.id;
-- Productos y detalles de su categoría
SELECT pr.nombre, pr.precio, cat.nombre AS nombre_categoria
FROM productos pr
INNER JOIN categorias cat ON pr.categoria_id = cat.id;
LEFT JOIN
Devuelve todas las filas de la tabla izquierda y las filas coincidentes de la derecha (NULL si no hay coincidencia):
-- Todos los clientes, incluso los que no tienen pedidos
SELECT
c.nombre,
c.apellido,
COUNT(p.id) AS cantidad_pedidos,
COALESCE(SUM(p.total), 0) AS total_gastado
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre, c.apellido;
-- Clientes que NUNCA han hecho un pedido
SELECT c.id, c.nombre, c.apellido
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
WHERE p.id IS NULL;
Múltiples JOINs
-- Pedidos con cliente + items + productos
SELECT
p.id AS id_pedido,
c.nombre AS cliente,
pr.nombre AS producto,
ip.cantidad,
ip.cantidad * ip.precio_unitario AS total_linea
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id
JOIN items_pedido ip ON p.id = ip.pedido_id
JOIN productos pr ON ip.producto_id = pr.id
ORDER BY p.id, pr.nombre;
SELF JOIN
Una tabla unida consigo misma — útil para datos jerárquicos:
CREATE TABLE empleados (
id INT PRIMARY KEY,
nombre VARCHAR(100),
gerente_id INT REFERENCES empleados(id)
);
SELECT
e.nombre AS empleado,
g.nombre AS gerente
FROM empleados e
LEFT JOIN empleados g ON e.gerente_id = g.id;
Funciones de Agregación
-- COUNT: número de filas
SELECT COUNT(*) FROM productos;
SELECT COUNT(telefono) FROM clientes; -- ignora NULL
-- SUM, AVG, MIN, MAX
SELECT
SUM(total) AS ingresos,
AVG(total) AS promedio_pedido,
MIN(total) AS pedido_minimo,
MAX(total) AS pedido_maximo
FROM pedidos;
GROUP BY
-- Total de ventas por categoría
SELECT
pr.categoria,
COUNT(ip.id) AS items_vendidos,
SUM(ip.cantidad) AS unidades_vendidas,
SUM(ip.cantidad * ip.precio_unitario) AS ingresos
FROM items_pedido ip
JOIN productos pr ON ip.producto_id = pr.id
GROUP BY pr.categoria;
-- Pedidos por cliente por mes
SELECT
cliente_id,
DATE_FORMAT(creado_en, '%Y-%m') AS mes,
COUNT(*) AS pedidos,
SUM(total) AS gasto_mensual
FROM pedidos
GROUP BY cliente_id, DATE_FORMAT(creado_en, '%Y-%m');
HAVING (filtrar después de GROUP BY)
-- Categorías con más de 100 unidades vendidas
SELECT
pr.categoria,
SUM(ip.cantidad) AS total_vendido
FROM items_pedido ip
JOIN productos pr ON ip.producto_id = pr.id
GROUP BY pr.categoria
HAVING SUM(ip.cantidad) > 100
ORDER BY total_vendido DESC;
-- Clientes que gastaron más de $1000
SELECT
cliente_id,
SUM(total) AS total_gastado
FROM pedidos
GROUP BY cliente_id
HAVING SUM(total) > 1000
ORDER BY total_gastado DESC;
WHERE vs HAVING
-- WHERE filtra filas individuales ANTES de agrupar
-- HAVING filtra grupos DESPUES de agrupar
SELECT categoria, AVG(precio) AS precio_promedio
FROM productos
WHERE stock > 0 -- primero filtrar filas
GROUP BY categoria
HAVING AVG(precio) > 50; -- luego filtrar grupos