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